Trò chơi Tic-Tac-Toe, game đánh caro full source code
- NetworkingPeer.cs
- PhotonNetwork /
- Plugins /
- Photon Unity Networking /
- Assets /
- project /
2 // <copyright file="NetworkingPeer.cs" company="Exit Games GmbH">
3 // Part of: Photon Unity Networking (PUN)
4 // </copyright>
5 // --------------------------------------------------------------------------------------------------------------------
6
7 using ExitGames.Client.Photon;
8 using ExitGames.Client.Photon.Lite;
9 using System;
10 using System.Collections;
11 using System.Collections.Generic;
12 using System.Reflection;
13 using UnityEngine;
14 using Hashtable = ExitGames.Client.Photon.Hashtable;
15
16 /// <summary>
17 /// Implements Photon LoadBalancing used in PUN.
18 /// This class is used internally by PhotonNetwork and not intended as public API.
19 /// </summary>
20 internal class NetworkingPeer : LoadbalancingPeer, IPhotonPeerListener
21 {
22 /// <summary>The GameVersion as set in the Connect-methods.</summary>
23 protected internal string mAppVersion;
24
25 /// <summary>Combination of GameVersion+"_"+PunVersion. Separates players per app by version.</summary>
26 protected internal string mAppVersionPun
27 {
28 get { return string.Format("{0}_{1}", mAppVersion, PhotonNetwork.versionPUN); }
29 }
30
31 /// <summary>Contains the AppId for the Photon Cloud (ignored by Photon Servers).</summary>
32 protected internal string mAppId;
33
34 /// <summary>
35 /// A user's authentication values used during connect for Custom Authentication with Photon (and a custom service/community).
36 /// Set these before calling Connect if you want custom authentication.
37 /// </summary>
38 public AuthenticationValues CustomAuthenticationValues { get; set; }
39
40 public string MasterServerAddress { get; protected internal set; }
41
42 private string playername = "";
43
44 private IPhotonPeerListener externalListener;
45
46 private JoinType mLastJoinType;
47
48 private bool mPlayernameHasToBeUpdated;
49
50 public string PlayerName
51 {
52 get
53 {
54 return this.playername;
55 }
56
57 set
58 {
59 if (string.IsNullOrEmpty(value) || value.Equals(this.playername))
60 {
61 return;
62 }
63
64 if (this.mLocalActor != null)
65 {
66 this.mLocalActor.name = value;
67 }
68
69 this.playername = value;
70 if (this.mCurrentGame != null)
71 {
72 // Only when in a room
73 this.SendPlayerName();
74 }
75 }
76 }
77
78 public PeerState State { get; internal set; }
79
80 // "public" access to the current game - is null unless a room is joined on a gameserver
81 public Room mCurrentGame
82 {
83 get
84 {
85 if (this.mRoomToGetInto != null && this.mRoomToGetInto.isLocalClientInside)
86 {
87 return this.mRoomToGetInto;
88 }
89
90 return null;
91 }
92 }
93
94 /// <summary>
95 /// keeps the custom properties, gameServer address and anything else about the room we want to get into
96 /// </summary>
97 internal Room mRoomToGetInto { get; set; }
98 internal RoomOptions mRoomOptionsForCreate { get; set; }
99 internal TypedLobby mRoomToEnterLobby { get; set; }
100
101 public Dictionary<int, PhotonPlayer> mActors = new Dictionary<int, PhotonPlayer>();
102
103 public PhotonPlayer[] mOtherPlayerListCopy = new PhotonPlayer[0];
104 public PhotonPlayer[] mPlayerListCopy = new PhotonPlayer[0];
105
106 public PhotonPlayer mLocalActor { get; internal set; }
107
108 public PhotonPlayer mMasterClient = null;
109
110 public bool hasSwitchedMC = false;
111
112 public string mGameserver { get; internal set; }
113
114 public bool requestSecurity = true;
115
116 private Dictionary<Type, List<MethodInfo>> monoRPCMethodsCache = new Dictionary<Type, List<MethodInfo>>();
117
118 public static bool UsePrefabCache = true;
119
120 public static Dictionary<string, GameObject> PrefabCache = new Dictionary<string, GameObject>();
121
122 public Dictionary<string, RoomInfo> mGameList = new Dictionary<string, RoomInfo>();
123 public RoomInfo[] mGameListCopy = new RoomInfo[0];
124
125 public TypedLobby lobby { get; set; }
126
127 public bool insideLobby = false;
128
129 /// <summary>Stat value: Count of players on Master (looking for rooms)</summary>
130 public int mPlayersOnMasterCount { get; internal set; }
131
132 /// <summary>Stat value: Count of Rooms</summary>
133 public int mGameCount { get; internal set; }
134
135 /// <summary>Stat value: Count of Players in rooms</summary>
136 public int mPlayersInRoomsCount { get; internal set; }
137
138 private HashSet<int> allowedReceivingGroups = new HashSet<int>();
139
140 private HashSet<int> blockSendingGroups = new HashSet<int>();
141
142 internal protected Dictionary<int, PhotonView> photonViewList = new Dictionary<int, PhotonView>(); //TODO: make private again
143
144 private readonly Dictionary<int, Hashtable> dataPerGroupReliable = new Dictionary<int, Hashtable>(); // only used in RunViewUpdate()
145 private readonly Dictionary<int, Hashtable> dataPerGroupUnreliable = new Dictionary<int, Hashtable>(); // only used in RunViewUpdate()
146
147 internal protected short currentLevelPrefix = 0;
148
149 private readonly Dictionary<string, int> rpcShortcuts; // lookup "table" for the index (shortcut) of an RPC name
150
151 /// <summary>The server this client is currently connected or connecting to.</summary>
152 internal protected ServerConnection server { get; private set; }
153
154 public bool IsInitialConnect = false;
155
156 /// <summary>True if this client uses a NameServer to get the Master Server address.</summary>
157 public bool IsUsingNameServer { get; protected internal set; }
158
159 /// <summary>Name Server Address that this client uses. You can use the default values and usually won't have to set this value.</summary>
160 public string NameServerAddress = "ns.exitgamescloud.com";
161 public string NameServerAddressHttp = "http://ns.exitgamescloud.com:80/photon/n";
162
163 private static readonly Dictionary<ConnectionProtocol, int> ProtocolToNameServerPort = new Dictionary<ConnectionProtocol, int>() { {ConnectionProtocol.Udp, 5058}, {ConnectionProtocol.Tcp, 4533} }; //, { ConnectionProtocol.RHttp, 6063 } };
164
165 /// <summary>A list of region names for the Photon Cloud. Set by the result of OpGetRegions().</summary>
166 /// <remarks>Put a "case OperationCode.GetRegions:" into your OnOperationResponse method to notice when the result is available.</remarks>
167 public List<Region> AvailableRegions { get; protected internal set; }
168
169 /// <summary>The cloud region this client connects to. Set by ConnectToRegionMaster().</summary>
170 public CloudRegionCode CloudRegion { get; protected internal set; }
171
172 /// <summary>Internally used to check if a "Secret" is available to use. Sent by Photon Cloud servers, it simplifies authentication when switching servers.</summary>
173 public bool IsAuthorizeSecretAvailable
174 {
175 get
176 {
177 // TODO: comment in the code again, when the new auth-workflow is available in the Cloud
178 return false; // this.CustomAuthenticationValues != null && !String.IsNullOrEmpty(this.CustomAuthenticationValues.Secret);
179 }
180 }
181
182 /// <summary>Internally used to trigger OpAuthenticate when encryption was established after a connect.</summary>
183 private bool didAuthenticate;
184
185 public NetworkingPeer(IPhotonPeerListener listener, string playername, ConnectionProtocol connectionProtocol) : base(listener, connectionProtocol)
186 {
187 #if !UNITY_EDITOR && (UNITY_WINRT)
188 // this automatically uses a separate assembly-file with Win8-style Socket usage (not possible in Editor)
189 Debug.LogWarning("Using PingWindowsStore");
190 PhotonHandler.PingImplementation = typeof(PingWindowsStore); // but for ping, we have to set the implementation explicitly to Win 8 Store/Phone
191 #endif
192
193 #pragma warning disable 0162 // the library variant defines if we should use PUN's SocketUdp variant (at all)
194 if (PhotonPeer.NoSocket)
195 {
196 #if !UNITY_EDITOR && (UNITY_PS3 || UNITY_ANDROID)
197 Debug.Log("Using class SocketUdpNativeDynamic");
198 this.SocketImplementation = typeof(SocketUdpNativeDynamic);
199 PhotonHandler.PingImplementation = typeof(PingNativeDynamic);
200 #elif !UNITY_EDITOR && UNITY_IPHONE
201 Debug.Log("Using class SocketUdpNativeStatic");
202 this.SocketImplementation = typeof(SocketUdpNativeStatic);
203 PhotonHandler.PingImplementation = typeof(PingNativeStatic);
204 #elif !UNITY_EDITOR && (UNITY_WINRT)
205 // this automatically uses a separate assembly-file with Win8-style Socket usage (not possible in Editor)
206 #else
207 this.SocketImplementation = typeof (SocketUdp);
208 PhotonHandler.PingImplementation = typeof(PingMonoEditor);
209 #endif
210
211 if (this.SocketImplementation == null)
212 {
213 Debug.Log("No socket implementation set for 'NoSocket' assembly. Please contact Exit Games.");
214 }
215 }
216 #pragma warning restore 0162
217
218 if (PhotonHandler.PingImplementation == null)
219 {
220 PhotonHandler.PingImplementation = typeof(PingMono);
221 }
222
223 this.Listener = this;
224 this.lobby = TypedLobby.Default;
225 this.LimitOfUnreliableCommands = 40;
226
227 // don't set the field directly! the listener is passed on to other classes, which get updated by the property set method
228 this.externalListener = listener;
229 this.PlayerName = playername;
230 this.mLocalActor = new PhotonPlayer(true, -1, this.playername);
231 this.AddNewPlayer(this.mLocalActor.ID, this.mLocalActor);
232
233 // RPC shortcut lookup creation (from list of RPCs, which is updated by Editor scripts)
234 rpcShortcuts = new Dictionary<string, int>(PhotonNetwork.PhotonServerSettings.RpcList.Count);
235 for (int index = 0; index < PhotonNetwork.PhotonServerSettings.RpcList.Count; index++)
236 {
237 var name = PhotonNetwork.PhotonServerSettings.RpcList[index];
238 rpcShortcuts[name] = index;
239 }
240
241 this.State = global::PeerState.PeerCreated;
242 }
243
244 #region Operations and Connection Methods
245
246
247 public override bool Connect(string serverAddress, string applicationName)
248 {
249 Debug.LogError("Avoid using this directly. Thanks.");
250 return false;
251 }
252
253 public bool Connect(string serverAddress, ServerConnection type)
254 {
255 if (PhotonHandler.AppQuits)
256 {
257 Debug.LogWarning("Ignoring Connect() because app gets closed. If this is an error, check PhotonHandler.AppQuits.");
258 return false;
259 }
260
261 if (PhotonNetwork.connectionStateDetailed == global::PeerState.Disconnecting)
262 {
263 Debug.LogError("Connect() failed. Can't connect while disconnecting (still). Current state: " + PhotonNetwork.connectionStateDetailed);
264 return false;
265 }
266
267 // connect might fail, if the DNS name can't be resolved or if no network connection is available
268 bool connecting = base.Connect(serverAddress, "");
269 if (connecting)
270 {
271 switch (type)
272 {
273 case ServerConnection.NameServer:
274 State = global::PeerState.ConnectingToNameServer;
275 break;
276 case ServerConnection.MasterServer:
277 State = global::PeerState.ConnectingToMasterserver;
278 break;
279 case ServerConnection.GameServer:
280 State = global::PeerState.ConnectingToGameserver;
281 break;
282 }
283 }
284
285 return connecting;
286 }
287
288
289 /// <summary>
290 /// Connects to the NameServer for Photon Cloud, where a region and server list can be obtained.
291 /// </summary>
292 /// <see cref="OpGetRegions"/>
293 /// <returns>If the workflow was started or failed right away.</returns>
294 public bool ConnectToNameServer()
295 {
296 if (PhotonHandler.AppQuits)
297 {
298 Debug.LogWarning("Ignoring Connect() because app gets closed. If this is an error, check PhotonHandler.AppQuits.");
299 return false;
300 }
301
302 IsUsingNameServer = true;
303 this.CloudRegion = CloudRegionCode.none;
304
305 if (this.State == global::PeerState.ConnectedToNameServer)
306 {
307 return true;
308 }
309
310 #if RHTTP
311 string address = (this.UsedProtocol == ConnectionProtocol.RHttp) ? this.NameServerAddressHttp : this.NameServerAddress;
312 #else
313 string address = this.NameServerAddress;
314 #endif
315
316 if (!address.Contains(":"))
317 {
318 int port = 0;
319 ProtocolToNameServerPort.TryGetValue(this.UsedProtocol, out port);
320 address = string.Format("{0}:{1}", address, port);
321 Debug.Log("Server to connect to: " + address + " settings protocol: " + PhotonNetwork.PhotonServerSettings.Protocol);
322 }
323 if (!base.Connect(address, "ns"))
324 {
325 return false;
326 }
327
328 this.State = global::PeerState.ConnectingToNameServer;
329 return true;
330 }
331
332 /// <summary>
333 /// Connects you to a specific region's Master Server, using the Name Server to find the IP.
334 /// </summary>
335 /// <returns>If the operation could be sent. If false, no operation was sent.</returns>
336 public bool ConnectToRegionMaster(CloudRegionCode region)
337 {
338 if (PhotonHandler.AppQuits)
339 {
340 Debug.LogWarning("Ignoring Connect() because app gets closed. If this is an error, check PhotonHandler.AppQuits.");
341 return false;
342 }
343
344 IsUsingNameServer = true;
345 this.CloudRegion = region;
346
347 if (this.State == global::PeerState.ConnectedToNameServer)
348 {
349 return this.OpAuthenticate(this.mAppId, this.mAppVersionPun, this.PlayerName, this.CustomAuthenticationValues, region.ToString());
350 }
351
352 #if RHTTP
353 string address = (this.UsedProtocol == ConnectionProtocol.RHttp) ? this.NameServerAddressHttp : this.NameServerAddress;
354 #else
355 string address = this.NameServerAddress;
356 #endif
357
358 if (!address.Contains(":"))
359 {
360 int port = 0;
361 ProtocolToNameServerPort.TryGetValue(this.UsedProtocol, out port);
362 address = string.Format("{0}:{1}", address, port);
363 //Debug.Log("Server to connect to: "+ address + " settings protocol: " + PhotonNetwork.PhotonServerSettings.Protocol);
364 }
365 if (!base.Connect(address, "ns"))
366 {
367 return false;
368 }
369
370 this.State = global::PeerState.ConnectingToNameServer;
371 return true;
372 }
373
374 /// <summary>
375 /// While on the NameServer, this gets you the list of regional servers (short names and their IPs to ping them).
376 /// </summary>
377 /// <returns>If the operation could be sent. If false, no operation was sent (e.g. while not connected to the NameServer).</returns>
378 public bool GetRegions()
379 {
380 if (this.server != ServerConnection.NameServer)
381 {
382 return false;
383 }
384
385 bool sent = this.OpGetRegions(this.mAppId);
386 if (sent)
387 {
388 this.AvailableRegions = null;
389 }
390
391 return sent;
392 }
393
394 /// <summary>
395 /// Complete disconnect from photon (and the open master OR game server)
396 /// </summary>
397 public override void Disconnect()
398 {
399 if (this.PeerState == PeerStateValue.Disconnected)
400 {
401 if (!PhotonHandler.AppQuits)
402 {
403 Debug.LogWarning(string.Format("Can't execute Disconnect() while not connected. Nothing changed. State: {0}", this.State));
404 }
405 return;
406 }
407
408 this.State = global::PeerState.Disconnecting;
409 base.Disconnect();
410
411 //this.LeftRoomCleanup();
412 //this.LeftLobbyCleanup();
413 }
414
415
416 /// <summary>
417 /// Internally used only. Triggers OnStateChange with "Disconnect" in next dispatch which is the signal to re-connect (if at all).
418 /// </summary>
419 private void DisconnectToReconnect()
420 {
421 switch (this.server)
422 {
423 case ServerConnection.NameServer:
424 this.State = global::PeerState.DisconnectingFromNameServer;
425 base.Disconnect();
426 break;
427 case ServerConnection.MasterServer:
428 this.State = global::PeerState.DisconnectingFromMasterserver;
429 base.Disconnect();
430 //LeftLobbyCleanup();
431 break;
432 case ServerConnection.GameServer:
433 this.State = global::PeerState.DisconnectingFromGameserver;
434 base.Disconnect();
435 //this.LeftRoomCleanup();
436 break;
437 }
438 }
439
440 /// <summary>
441 /// Called at disconnect/leavelobby etc. This CAN also be called when we are not in a lobby (e.g. disconnect from room)
442 /// </summary>
443 /// <remarks>Calls callback method OnLeftLobby if this client was in a lobby initially. Clears the lobby's game lists.</remarks>
444 private void LeftLobbyCleanup()
445 {
446 this.mGameList = new Dictionary<string, RoomInfo>();
447 this.mGameListCopy = new RoomInfo[0];
448
449 if (insideLobby)
450 {
451 this.insideLobby = false;
452 SendMonoMessage(PhotonNetworkingMessage.OnLeftLobby);
453 }
454 }
455
456 /// <summary>
457 /// Called when "this client" left a room to clean up.
458 /// </summary>
459 private void LeftRoomCleanup()
460 {
461 bool wasInRoom = mRoomToGetInto != null;
462 // when leaving a room, we clean up depending on that room's settings.
463 bool autoCleanupSettingOfRoom = (this.mRoomToGetInto != null) ? this.mRoomToGetInto.autoCleanUp : PhotonNetwork.autoCleanUpPlayerObjects;
464
465 this.hasSwitchedMC = false;
466 this.mRoomToGetInto = null;
467 this.mActors = new Dictionary<int, PhotonPlayer>();
468 this.mPlayerListCopy = new PhotonPlayer[0];
469 this.mOtherPlayerListCopy = new PhotonPlayer[0];
470 this.mMasterClient = null;
471 this.allowedReceivingGroups = new HashSet<int>();
472 this.blockSendingGroups = new HashSet<int>();
473 this.mGameList = new Dictionary<string, RoomInfo>();
474 this.mGameListCopy = new RoomInfo[0];
475 this.isFetchingFriends = false;
476
477 this.ChangeLocalID(-1);
478
479 // Cleanup all network objects (all spawned PhotonViews, local and remote)
480 if (autoCleanupSettingOfRoom)
481 {
482 this.LocalCleanupAnythingInstantiated(true);
483 PhotonNetwork.manuallyAllocatedViewIds = new List<int>(); // filled and easier to replace completely
484 }
485
486 if (wasInRoom)
487 {
488 SendMonoMessage(PhotonNetworkingMessage.OnLeftRoom);
489 }
490 }
491
492 /// <summary>
493 /// Cleans up anything that was instantiated in-game (not loaded with the scene).
494 /// </summary>
495 protected internal void LocalCleanupAnythingInstantiated(bool destroyInstantiatedGameObjects)
496 {
497 if (tempInstantiationData.Count > 0)
498 {
499 Debug.LogWarning("It seems some instantiation is not completed, as instantiation data is used. You should make sure instantiations are paused when calling this method. Cleaning now, despite this.");
500 }
501
502 // Destroy GO's (if we should)
503 if (destroyInstantiatedGameObjects)
504 {
505 // Fill list with Instantiated objects
506 HashSet<GameObject> instantiatedGos = new HashSet<GameObject>();
507 foreach (PhotonView view in this.photonViewList.Values)
508 {
509 if (view.isRuntimeInstantiated)
510 {
511 instantiatedGos.Add(view.gameObject); // HashSet keeps each object only once
512 }
513 }
514
515 foreach (GameObject go in instantiatedGos)
516 {
517 this.RemoveInstantiatedGO(go, true);
518 }
519 }
520
521 // photonViewList is cleared of anything instantiated (so scene items are left inside)
522 // any other lists can be
523 this.tempInstantiationData.Clear(); // should be empty but to be safe we clear (no new list needed)
524 PhotonNetwork.lastUsedViewSubId = 0;
525 PhotonNetwork.lastUsedViewSubIdStatic = 0;
526 }
527
528 // gameID can be null (optional). The server assigns a unique name if no name is set
529
530 // joins a room and sets your current username as custom actorproperty (will broadcast that)
531
532 #endregion
533
534 #region Helpers
535
536 private void ReadoutProperties(Hashtable gameProperties, Hashtable pActorProperties, int targetActorNr)
537 {
538 // Debug.LogWarning("ReadoutProperties gameProperties: " + gameProperties.ToStringFull() + " pActorProperties: " + pActorProperties.ToStringFull() + " targetActorNr: " + targetActorNr);
539 // read game properties and cache them locally
540 if (this.mCurrentGame != null && gameProperties != null)
541 {
542 this.mCurrentGame.CacheProperties(gameProperties);
543 SendMonoMessage(PhotonNetworkingMessage.OnPhotonCustomRoomPropertiesChanged, gameProperties);
544 if (PhotonNetwork.automaticallySyncScene)
545 {
546 this.LoadLevelIfSynced(); // will load new scene if sceneName was changed
547 }
548 }
549
550 if (pActorProperties != null && pActorProperties.Count > 0)
551 {
552 if (targetActorNr > 0)
553 {
554 // we have a single entry in the pActorProperties with one
555 // user's name
556 // targets MUST exist before you set properties
557 PhotonPlayer target = this.GetPlayerWithID(targetActorNr);
558 if (target != null)
559 {
560 Hashtable props = this.GetActorPropertiesForActorNr(pActorProperties, targetActorNr);
561 target.InternalCacheProperties(props);
562 SendMonoMessage(PhotonNetworkingMessage.OnPhotonPlayerPropertiesChanged, target, props);
563 }
564 }
565 else
566 {
567 // in this case, we've got a key-value pair per actor (each
568 // value is a hashtable with the actor's properties then)
569 int actorNr;
570 Hashtable props;
571 string newName;
572 PhotonPlayer target;
573
574 foreach (object key in pActorProperties.Keys)
575 {
576 actorNr = (int)key;
577 props = (Hashtable)pActorProperties[key];
578 newName = (string)props[ActorProperties.PlayerName];
579
580 target = this.GetPlayerWithID(actorNr);
581 if (target == null)
582 {
583 target = new PhotonPlayer(false, actorNr, newName);
584 this.AddNewPlayer(actorNr, target);
585 }
586
587 target.InternalCacheProperties(props);
588 SendMonoMessage(PhotonNetworkingMessage.OnPhotonPlayerPropertiesChanged, target, props);
589 }
590 }
591 }
592 }
593
594 private void AddNewPlayer(int ID, PhotonPlayer player)
595 {
596 if (!this.mActors.ContainsKey(ID))
597 {
598 this.mActors[ID] = player;
599 RebuildPlayerListCopies();
600 }
601 else
602 {
603 Debug.LogError("Adding player twice: " + ID);
604 }
605 }
606
607 void RemovePlayer(int ID, PhotonPlayer player)
608 {
609 this.mActors.Remove(ID);
610 if (!player.isLocal)
611 {
612 RebuildPlayerListCopies();
613 }
614 }
615
616 void RebuildPlayerListCopies()
617 {
618 this.mPlayerListCopy = new PhotonPlayer[this.mActors.Count];
619 this.mActors.Values.CopyTo(this.mPlayerListCopy, 0);
620
621 List<PhotonPlayer> otherP = new List<PhotonPlayer>();
622 foreach (PhotonPlayer player in this.mPlayerListCopy)
623 {
624 if (!player.isLocal)
625 {
626 otherP.Add(player);
627 }
628 }
629
630 this.mOtherPlayerListCopy = otherP.ToArray();
631 }
632
633 /// <summary>
634 /// Resets the PhotonView "lastOnSerializeDataSent" so that "OnReliable" synched PhotonViews send a complete state to new clients (if the state doesnt change, no messages would be send otherwise!).
635 /// Note that due to this reset, ALL other players will receive the full OnSerialize.
636 /// </summary>
637 private void ResetPhotonViewsOnSerialize()
638 {
639 foreach (PhotonView photonView in this.photonViewList.Values)
640 {
641 photonView.lastOnSerializeDataSent = null;
642 }
643 }
644
645 /// <summary>
646 /// Called when the event Leave (of some other player) arrived.
647 /// Cleans game objects, views locally. The master will also clean the
648 /// </summary>
649 /// <param name="actorID">ID of player who left.</param>
650 private void HandleEventLeave(int actorID)
651 {
652 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
653 Debug.Log("HandleEventLeave for player ID: " + actorID);
654
655
656 // actorNr is fetched out of event above
657 if (actorID < 0 || !this.mActors.ContainsKey(actorID))
658 {
659 Debug.LogError(String.Format("Received event Leave for unknown player ID: {0}", actorID));
660 return;
661 }
662
663 PhotonPlayer player = this.GetPlayerWithID(actorID);
664 if (player == null)
665 {
666 Debug.LogError("HandleEventLeave for player ID: " + actorID + " has no PhotonPlayer!");
667 }
668
669 // having a new master before calling destroy for the leaving player is important!
670 // so we elect a new masterclient and ignore the leaving player (who is still in playerlists).
671 this.CheckMasterClient(actorID);
672
673
674 // destroy objects & buffered messages
675 if (this.mCurrentGame != null && this.mCurrentGame.autoCleanUp)
676 {
677 this.DestroyPlayerObjects(actorID, true);
678 }
679
680 RemovePlayer(actorID, player);
681
682 // finally, send notification (the playerList and masterclient are now updated)
683 SendMonoMessage(PhotonNetworkingMessage.OnPhotonPlayerDisconnected, player);
684 }
685
686 /// <summary>Picks the new master client from player list, if the current Master is leaving (leavingPlayerId) or if no master was assigned so far.</summary>
687 /// <param name="leavingPlayerId">
688 /// The ignored player is the one who's leaving and should not become master (again). Pass -1 to select any player from the list.
689 /// </param>
690 private void CheckMasterClient(int leavingPlayerId)
691 {
692 bool currentMasterIsLeaving = this.mMasterClient != null && this.mMasterClient.ID == leavingPlayerId;
693 bool someoneIsLeaving = leavingPlayerId > 0;
694
695 // return early if SOME player (leavingId > 0) is leaving AND it's NOT the current master
696 if (someoneIsLeaving && !currentMasterIsLeaving)
697 {
698 return;
699 }
700
701 // picking the player with lowest ID (longest in game).
702 if (this.mActors.Count <= 1)
703 {
704 this.mMasterClient = this.mLocalActor;
705 }
706 else
707 {
708 // keys in mActors are their actorNumbers
709 int lowestActorNumber = Int32.MaxValue;
710 foreach (int key in this.mActors.Keys)
711 {
712 if (key < lowestActorNumber && key != leavingPlayerId)
713 {
714 lowestActorNumber = key;
715 }
716 }
717
718 this.mMasterClient = this.mActors[lowestActorNumber];
719 }
720
721 // make a callback ONLY when a player/Master left
722 if (someoneIsLeaving)
723 {
724 SendMonoMessage(PhotonNetworkingMessage.OnMasterClientSwitched, this.mMasterClient);
725 }
726 }
727
728 /// <summary>
729 /// Returns the lowest player.ID - used for Master Client picking.
730 /// </summary>
731 /// <remarks></remarks>
732 private static int ReturnLowestPlayerId(PhotonPlayer[] players, int playerIdToIgnore)
733 {
734 if (players == null || players.Length == 0)
735 {
736 return -1;
737 }
738
739 int lowestActorNumber = Int32.MaxValue;
740 for (int i = 0; i < players.Length; i++)
741 {
742 PhotonPlayer photonPlayer = players[i];
743 if (photonPlayer.ID == playerIdToIgnore)
744 {
745 continue;
746 }
747
748 if (photonPlayer.ID < lowestActorNumber)
749 {
750 lowestActorNumber = photonPlayer.ID;
751 }
752 }
753
754 return lowestActorNumber;
755 }
756
757 internal protected bool SetMasterClient(int playerId, bool sync)
758 {
759 bool masterReplaced = this.mMasterClient != null && this.mMasterClient.ID != playerId;
760 if (!masterReplaced || !this.mActors.ContainsKey(playerId))
761 {
762 return false;
763 }
764
765 if (sync)
766 {
767 bool sent = this.OpRaiseEvent(PunEvent.AssignMaster, new Hashtable() { { (byte)1, playerId } }, true, null);
768 if (!sent)
769 {
770 return false;
771 }
772 }
773
774 this.hasSwitchedMC = true;
775 this.mMasterClient = this.mActors[playerId];
776 SendMonoMessage(PhotonNetworkingMessage.OnMasterClientSwitched, this.mMasterClient); // we only callback when an actual change is done
777 return true;
778 }
779
780 private Hashtable GetActorPropertiesForActorNr(Hashtable actorProperties, int actorNr)
781 {
782 if (actorProperties.ContainsKey(actorNr))
783 {
784 return (Hashtable)actorProperties[actorNr];
785 }
786
787 return actorProperties;
788 }
789
790 private PhotonPlayer GetPlayerWithID(int number)
791 {
792 if (this.mActors != null && this.mActors.ContainsKey(number))
793 {
794 return this.mActors[number];
795 }
796
797 return null;
798 }
799
800 private void SendPlayerName()
801 {
802 if (this.State == global::PeerState.Joining)
803 {
804 // this means, the join on the gameServer is sent (with an outdated name). send the new when in game
805 this.mPlayernameHasToBeUpdated = true;
806 return;
807 }
808
809 if (this.mLocalActor != null)
810 {
811 this.mLocalActor.name = this.PlayerName;
812 Hashtable properties = new Hashtable();
813 properties[ActorProperties.PlayerName] = this.PlayerName;
814 if (this.mLocalActor.ID > 0)
815 {
816 this.OpSetPropertiesOfActor(this.mLocalActor.ID, properties, true, (byte)0);
817 this.mPlayernameHasToBeUpdated = false;
818 }
819 }
820 }
821
822 private void GameEnteredOnGameServer(OperationResponse operationResponse)
823 {
824 if (operationResponse.ReturnCode != 0)
825 {
826 switch (operationResponse.OperationCode)
827 {
828 case OperationCode.CreateGame:
829 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
830 {
831 Debug.Log("Create failed on GameServer. Changing back to MasterServer. Msg: " + operationResponse.DebugMessage);
832 }
833 SendMonoMessage(PhotonNetworkingMessage.OnPhotonCreateRoomFailed, operationResponse.ReturnCode, operationResponse.DebugMessage);
834 break;
835 case OperationCode.JoinGame:
836 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
837 {
838 Debug.Log("Join failed on GameServer. Changing back to MasterServer. Msg: " + operationResponse.DebugMessage);
839 if (operationResponse.ReturnCode == ErrorCode.GameDoesNotExist)
840 {
841 Debug.Log("Most likely the game became empty during the switch to GameServer.");
842 }
843 }
844 SendMonoMessage(PhotonNetworkingMessage.OnPhotonJoinRoomFailed, operationResponse.ReturnCode, operationResponse.DebugMessage);
845 break;
846 case OperationCode.JoinRandomGame:
847 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
848 {
849 Debug.Log("Join failed on GameServer. Changing back to MasterServer. Msg: " + operationResponse.DebugMessage);
850 if (operationResponse.ReturnCode == ErrorCode.GameDoesNotExist)
851 {
852 Debug.Log("Most likely the game became empty during the switch to GameServer.");
853 }
854 }
855 SendMonoMessage(PhotonNetworkingMessage.OnPhotonRandomJoinFailed, operationResponse.ReturnCode, operationResponse.DebugMessage);
856 break;
857 }
858
859 this.DisconnectToReconnect();
860 return;
861 }
862
863 this.State = global::PeerState.Joined;
864 this.mRoomToGetInto.isLocalClientInside = true;
865
866 Hashtable actorProperties = (Hashtable)operationResponse[ParameterCode.PlayerProperties];
867 Hashtable gameProperties = (Hashtable)operationResponse[ParameterCode.GameProperties];
868 this.ReadoutProperties(gameProperties, actorProperties, 0);
869
870 // the local player's actor-properties are not returned in join-result. add this player to the list
871 int localActorNr = (int)operationResponse[ParameterCode.ActorNr];
872
873 this.ChangeLocalID(localActorNr);
874 this.CheckMasterClient(-1);
875
876 if (this.mPlayernameHasToBeUpdated)
877 {
878 this.SendPlayerName();
879 }
880
881 switch (operationResponse.OperationCode)
882 {
883 case OperationCode.CreateGame:
884 SendMonoMessage(PhotonNetworkingMessage.OnCreatedRoom);
885 break;
886 case OperationCode.JoinGame:
887 case OperationCode.JoinRandomGame:
888 // the mono message for this is sent at another place
889 break;
890 }
891 }
892
893 private Hashtable GetLocalActorProperties()
894 {
895 if (PhotonNetwork.player != null)
896 {
897 return PhotonNetwork.player.allProperties;
898 }
899
900 Hashtable actorProperties = new Hashtable();
901 actorProperties[ActorProperties.PlayerName] = this.PlayerName;
902 return actorProperties;
903 }
904
905 public void ChangeLocalID(int newID)
906 {
907 if (this.mLocalActor == null)
908 {
909 Debug.LogWarning(
910 string.Format(
911 "Local actor is null or not in mActors! mLocalActor: {0} mActors==null: {1} newID: {2}",
912 this.mLocalActor,
913 this.mActors == null,
914 newID));
915 }
916
917 if (this.mActors.ContainsKey(this.mLocalActor.ID))
918 {
919 this.mActors.Remove(this.mLocalActor.ID);
920 }
921
922 this.mLocalActor.InternalChangeLocalID(newID);
923 this.mActors[this.mLocalActor.ID] = this.mLocalActor;
924 this.RebuildPlayerListCopies();
925 }
926
927 #endregion
928
929 #region Operations
930
931 /// <summary>NetworkingPeer.OpCreateGame</summary>
932 public bool OpCreateGame(string roomName, RoomOptions roomOptions, TypedLobby typedLobby)
933 {
934 bool onGameServer = this.server == ServerConnection.GameServer;
935 if (!onGameServer)
936 {
937 this.mRoomOptionsForCreate = roomOptions;
938 this.mRoomToGetInto = new Room(roomName, roomOptions);
939 this.mRoomToEnterLobby = typedLobby ?? ((this.insideLobby) ? this.lobby : null); // use given lobby, or active lobby (if any active) or none
940 }
941
942 this.mLastJoinType = JoinType.CreateGame;
943 return base.OpCreateRoom(roomName, roomOptions, this.mRoomToEnterLobby, this.GetLocalActorProperties(), onGameServer);
944 }
945
946 /// <summary>NetworkingPeer.OpJoinRoom</summary>
947 public bool OpJoinRoom(string roomName, RoomOptions roomOptions, TypedLobby typedLobby, bool createIfNotExists)
948 {
949 bool onGameServer = this.server == ServerConnection.GameServer;
950 if (!onGameServer)
951 {
952 // roomOptions and typedLobby will be null, unless createIfNotExists is true
953 this.mRoomOptionsForCreate = roomOptions;
954 this.mRoomToGetInto = new Room(roomName, roomOptions);
955 this.mRoomToEnterLobby = null;
956 if (createIfNotExists)
957 {
958 this.mRoomToEnterLobby = typedLobby ?? ((this.insideLobby) ? this.lobby : null); // use given lobby, or active lobby (if any active) or none
959 }
960 }
961
962 this.mLastJoinType = (createIfNotExists) ? JoinType.JoinOrCreateOnDemand : JoinType.JoinGame;
963 return base.OpJoinRoom(roomName, roomOptions, this.mRoomToEnterLobby, createIfNotExists, this.GetLocalActorProperties(), onGameServer);
964 }
965
966 /// <summary>NetworkingPeer.OpJoinRandomRoom</summary>
967 /// <remarks>this override just makes sure we have a mRoomToGetInto, even if it's blank (the properties provided in this method are filters. they are not set when we join the game)</remarks>
968 public override bool OpJoinRandomRoom(Hashtable expectedCustomRoomProperties, byte expectedMaxPlayers, Hashtable playerProperties, MatchmakingMode matchingType, TypedLobby typedLobby, string sqlLobbyFilter)
969 {
970 this.mRoomToGetInto = new Room(null, null);
971 this.mRoomToEnterLobby = null; // join random never stores the lobby. the following join will not affect the room lobby
972 // if typedLobby is null, the server will automatically use the active lobby or default, which is what we want anyways
973
974 this.mLastJoinType = JoinType.JoinRandomGame;
975 return base.OpJoinRandomRoom(expectedCustomRoomProperties, expectedMaxPlayers, playerProperties, matchingType, typedLobby, sqlLobbyFilter);
976 }
977
978 /// <summary>
979 /// Operation Leave will exit any current room.
980 /// </summary>
981 /// <remarks>
982 /// This also happens when you disconnect from the server.
983 /// Disconnect might be a step less if you don't want to create a new room on the same server.
984 /// </remarks>
985 /// <returns></returns>
986 public virtual bool OpLeave()
987 {
988 if (this.State != global::PeerState.Joined)
989 {
990 Debug.LogWarning("Not sending leave operation. State is not 'Joined': " + this.State);
991 return false;
992 }
993
994 return this.OpCustom((byte)OperationCode.Leave, null, true, 0);
995 }
996
997 public override bool OpRaiseEvent(byte eventCode, object customEventContent, bool sendReliable, RaiseEventOptions raiseEventOptions)
998 {
999 if (PhotonNetwork.offlineMode)
1000 {
1001 return false;
1002 }
1003
1004 return base.OpRaiseEvent(eventCode, customEventContent, sendReliable, raiseEventOptions);
1005 }
1006
1007 #endregion
1008
1009 #region Implementation of IPhotonPeerListener
1010
1011 public void DebugReturn(DebugLevel level, string message)
1012 {
1013 this.externalListener.DebugReturn(level, message);
1014 }
1015
1016 public void OnOperationResponse(OperationResponse operationResponse)
1017 {
1018 if (PhotonNetwork.networkingPeer.State == global::PeerState.Disconnecting)
1019 {
1020 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
1021 {
1022 Debug.Log("OperationResponse ignored while disconnecting. Code: " + operationResponse.OperationCode);
1023 }
1024 return;
1025 }
1026
1027 // extra logging for error debugging (helping developers with a bit of automated analysis)
1028 if (operationResponse.ReturnCode == 0)
1029 {
1030 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
1031 Debug.Log(operationResponse.ToString());
1032 }
1033 else
1034 {
1035 if (operationResponse.ReturnCode == ErrorCode.OperationNotAllowedInCurrentState)
1036 {
1037 Debug.LogError("Operation " + operationResponse.OperationCode + " could not be executed (yet). Wait for state JoinedLobby or ConnectedToMaster and their callbacks before calling operations. WebRPCs need a server-side configuration. Enum OperationCode helps identify the operation.");
1038 }
1039 else if (operationResponse.ReturnCode == ErrorCode.WebHookCallFailed)
1040 {
1041 Debug.LogError("Operation " + operationResponse.OperationCode + " failed in a server-side plugin. Check the configuration in the Dashboard. Message from server-plugin: " + operationResponse.DebugMessage);
1042 }
1043 else if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
1044 {
1045 Debug.LogError("Operation failed: " + operationResponse.ToStringFull() + " Server: " + this.server);
1046 }
1047 }
1048
1049 // use the "secret" or "token" whenever we get it. doesn't really matter if it's in AuthResponse.
1050 if (operationResponse.Parameters.ContainsKey(ParameterCode.Secret))
1051 {
1052 if (this.CustomAuthenticationValues == null)
1053 {
1054 this.CustomAuthenticationValues = new AuthenticationValues();
1055 // this.DebugReturn(DebugLevel.ERROR, "Server returned secret. Created CustomAuthenticationValues.");
1056 }
1057
1058 this.CustomAuthenticationValues.Secret = operationResponse[ParameterCode.Secret] as string;
1059 }
1060
1061 switch (operationResponse.OperationCode)
1062 {
1063 case OperationCode.Authenticate:
1064 {
1065 // PeerState oldState = this.State;
1066
1067 if (operationResponse.ReturnCode != 0)
1068 {
1069 if (operationResponse.ReturnCode == ErrorCode.InvalidOperationCode)
1070 {
1071 Debug.LogError(string.Format("If you host Photon yourself, make sure to start the 'Instance LoadBalancing' "+ this.ServerAddress));
1072 }
1073 else if (operationResponse.ReturnCode == ErrorCode.InvalidAuthentication)
1074 {
1075 Debug.LogError(string.Format("The appId this client sent is unknown on the server (Cloud). Check settings. If using the Cloud, check account."));
1076 SendMonoMessage(PhotonNetworkingMessage.OnFailedToConnectToPhoton, DisconnectCause.InvalidAuthentication);
1077 }
1078 else if (operationResponse.ReturnCode == ErrorCode.CustomAuthenticationFailed)
1079 {
1080 Debug.LogError(string.Format("Custom Authentication failed (either due to user-input or configuration or AuthParameter string format). Calling: OnCustomAuthenticationFailed()"));
1081 SendMonoMessage(PhotonNetworkingMessage.OnCustomAuthenticationFailed, operationResponse.DebugMessage);
1082 }
1083 else
1084 {
1085 Debug.LogError(string.Format("Authentication failed: '{0}' Code: {1}", operationResponse.DebugMessage, operationResponse.ReturnCode));
1086 }
1087
1088 this.State = global::PeerState.Disconnecting;
1089 this.Disconnect();
1090
1091 if (operationResponse.ReturnCode == ErrorCode.MaxCcuReached)
1092 {
1093 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
1094 Debug.LogWarning(string.Format("Currently, the limit of users is reached for this title. Try again later. Disconnecting"));
1095 SendMonoMessage(PhotonNetworkingMessage.OnPhotonMaxCccuReached);
1096 SendMonoMessage(PhotonNetworkingMessage.OnConnectionFail, DisconnectCause.MaxCcuReached);
1097 }
1098 else if (operationResponse.ReturnCode == ErrorCode.InvalidRegion)
1099 {
1100 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
1101 Debug.LogError(string.Format("The used master server address is not available with the subscription currently used. Got to Photon Cloud Dashboard or change URL. Disconnecting."));
1102 SendMonoMessage(PhotonNetworkingMessage.OnConnectionFail, DisconnectCause.InvalidRegion);
1103 }
1104 else if (operationResponse.ReturnCode == ErrorCode.AuthenticationTicketExpired)
1105 {
1106 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
1107 Debug.LogError(string.Format("The authentication ticket expired. You need to connect (and authenticate) again. Disconnecting."));
1108 SendMonoMessage(PhotonNetworkingMessage.OnConnectionFail, DisconnectCause.AuthenticationTicketExpired);
1109 }
1110 break;
1111 }
1112 else
1113 {
1114 if (this.server == ServerConnection.NameServer)
1115 {
1116 // on the NameServer, authenticate returns the MasterServer address for a region and we hop off to there
1117 this.MasterServerAddress = operationResponse[ParameterCode.Address] as string;
1118 this.DisconnectToReconnect();
1119 }
1120 else if (this.server == ServerConnection.MasterServer)
1121 {
1122 if (PhotonNetwork.autoJoinLobby)
1123 {
1124 this.State = global::PeerState.Authenticated;
1125 this.OpJoinLobby(this.lobby);
1126 }
1127 else
1128 {
1129 this.State = global::PeerState.ConnectedToMaster;
1130 NetworkingPeer.SendMonoMessage(PhotonNetworkingMessage.OnConnectedToMaster);
1131 }
1132 }
1133 else if (this.server == ServerConnection.GameServer)
1134 {
1135 this.State = global::PeerState.Joining;
1136
1137 if (this.mLastJoinType == JoinType.JoinGame || this.mLastJoinType == JoinType.JoinRandomGame || this.mLastJoinType == JoinType.JoinOrCreateOnDemand)
1138 {
1139 // if we just "join" the game, do so. if we wanted to "create the room on demand", we have to send this to the game server as well.
1140 this.OpJoinRoom(this.mRoomToGetInto.name, this.mRoomOptionsForCreate, this.mRoomToEnterLobby, this.mLastJoinType == JoinType.JoinOrCreateOnDemand);
1141 }
1142 else if (this.mLastJoinType == JoinType.CreateGame)
1143 {
1144 // on the game server, we have to apply the room properties that were chosen for creation of the room, so we use this.mRoomToGetInto
1145 this.OpCreateGame(this.mRoomToGetInto.name, this.mRoomOptionsForCreate, this.mRoomToEnterLobby);
1146 }
1147
1148 break;
1149 }
1150 }
1151 break;
1152 }
1153
1154 case OperationCode.GetRegions:
1155 // Debug.Log("GetRegions returned: " + operationResponse.ToStringFull());
1156
1157 if (operationResponse.ReturnCode == ErrorCode.InvalidAuthentication)
1158 {
1159 Debug.LogError(string.Format("The appId this client sent is unknown on the server (Cloud). Check settings. If using the Cloud, check account."));
1160 SendMonoMessage(PhotonNetworkingMessage.OnFailedToConnectToPhoton, DisconnectCause.InvalidAuthentication);
1161
1162 this.State = global::PeerState.Disconnecting;
1163 this.Disconnect();
1164 return;
1165 }
1166
1167 string[] regions = operationResponse[ParameterCode.Region] as string[];
1168 string[] servers = operationResponse[ParameterCode.Address] as string[];
1169
1170 if (regions == null || servers == null || regions.Length != servers.Length)
1171 {
1172 Debug.LogError("The region arrays from Name Server are not ok. Must be non-null and same length.");
1173 break;
1174 }
1175
1176 this.AvailableRegions = new List<Region>(regions.Length);
1177 for (int i = 0; i < regions.Length; i++)
1178 {
1179 string regionCodeString = regions[i];
1180 if (string.IsNullOrEmpty(regionCodeString))
1181 {
1182 continue;
1183 }
1184 regionCodeString = regionCodeString.ToLower();
1185
1186 CloudRegionCode code = Region.Parse(regionCodeString);
1187 this.AvailableRegions.Add(new Region() { Code = code, HostAndPort = servers[i] });
1188 }
1189
1190 // PUN assumes you fetch the name-server's list of regions to ping them
1191 if (PhotonNetwork.PhotonServerSettings.HostType == ServerSettings.HostingOption.BestRegion)
1192 {
1193 PhotonHandler.PingAvailableRegionsAndConnectToBest();
1194 }
1195 break;
1196
1197 case OperationCode.CreateGame:
1198 {
1199 if (this.server == ServerConnection.GameServer)
1200 {
1201 this.GameEnteredOnGameServer(operationResponse);
1202 }
1203 else
1204 {
1205 if (operationResponse.ReturnCode != 0)
1206 {
1207 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
1208 Debug.LogWarning(string.Format("CreateRoom failed, client stays on masterserver: {0}.", operationResponse.ToStringFull()));
1209
1210 SendMonoMessage(PhotonNetworkingMessage.OnPhotonCreateRoomFailed);
1211 break;
1212 }
1213
1214 string gameID = (string) operationResponse[ParameterCode.RoomName];
1215 if (!string.IsNullOrEmpty(gameID))
1216 {
1217 // is only sent by the server's response, if it has not been
1218 // sent with the client's request before!
1219 this.mRoomToGetInto.name = gameID;
1220 }
1221
1222 this.mGameserver = (string)operationResponse[ParameterCode.Address];
1223 this.DisconnectToReconnect();
1224 }
1225
1226 break;
1227 }
1228
1229 case OperationCode.JoinGame:
1230 {
1231 if (this.server != ServerConnection.GameServer)
1232 {
1233 if (operationResponse.ReturnCode != 0)
1234 {
1235 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
1236 Debug.Log(string.Format("JoinRoom failed (room maybe closed by now). Client stays on masterserver: {0}. State: {1}", operationResponse.ToStringFull(), this.State));
1237
1238 SendMonoMessage(PhotonNetworkingMessage.OnPhotonJoinRoomFailed);
1239 break;
1240 }
1241
1242 this.mGameserver = (string)operationResponse[ParameterCode.Address];
1243 this.DisconnectToReconnect();
1244 }
1245 else
1246 {
1247 this.GameEnteredOnGameServer(operationResponse);
1248 }
1249
1250 break;
1251 }
1252
1253 case OperationCode.JoinRandomGame:
1254 {
1255 // happens only on master. on gameserver, this is a regular join (we don't need to find a random game again)
1256 // the operation OpJoinRandom either fails (with returncode 8) or returns game-to-join information
1257 if (operationResponse.ReturnCode != 0)
1258 {
1259 if (operationResponse.ReturnCode == ErrorCode.NoRandomMatchFound)
1260 {
1261 if (PhotonNetwork.logLevel >= PhotonLogLevel.Full)
1262 Debug.Log("JoinRandom failed: No open game. Calling: OnPhotonRandomJoinFailed() and staying on master server.");
1263 }
1264 else if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
1265 {
1266 Debug.LogWarning(string.Format("JoinRandom failed: {0}.", operationResponse.ToStringFull()));
1267 }
1268
1269 SendMonoMessage(PhotonNetworkingMessage.OnPhotonRandomJoinFailed, operationResponse.ReturnCode, operationResponse.DebugMessage);
1270 break;
1271 }
1272
1273 string roomName = (string)operationResponse[ParameterCode.RoomName];
1274 this.mRoomToGetInto.name = roomName;
1275 this.mGameserver = (string)operationResponse[ParameterCode.Address];
1276 this.DisconnectToReconnect();
1277 break;
1278 }
1279
1280 case OperationCode.JoinLobby:
1281 this.State = global::PeerState.JoinedLobby;
1282 this.insideLobby = true;
1283 SendMonoMessage(PhotonNetworkingMessage.OnJoinedLobby);
1284
1285 // this.mListener.joinLobbyReturn();
1286 break;
1287 case OperationCode.LeaveLobby:
1288 this.State = global::PeerState.Authenticated;
1289 this.LeftLobbyCleanup(); // will set insideLobby = false
1290 break;
1291
1292 case OperationCode.Leave:
1293 this.DisconnectToReconnect();
1294 break;
1295
1296 case OperationCode.SetProperties:
1297 // this.mListener.setPropertiesReturn(returnCode, debugMsg);
1298 break;
1299
1300 case OperationCode.GetProperties:
1301 {
1302 Hashtable actorProperties = (Hashtable)operationResponse[ParameterCode.PlayerProperties];
1303 Hashtable gameProperties = (Hashtable)operationResponse[ParameterCode.GameProperties];
1304 this.ReadoutProperties(gameProperties, actorProperties, 0);
1305
1306 // RemoveByteTypedPropertyKeys(actorProperties, false);
1307 // RemoveByteTypedPropertyKeys(gameProperties, false);
1308 // this.mListener.getPropertiesReturn(gameProperties, actorProperties, returnCode, debugMsg);
1309 break;
1310 }
1311
1312 case OperationCode.RaiseEvent:
1313 // this usually doesn't give us a result. only if the caching is affected the server will send one.
1314 break;
1315
1316 case OperationCode.FindFriends:
1317 bool[] onlineList = operationResponse[ParameterCode.FindFriendsResponseOnlineList] as bool[];
1318 string[] roomList = operationResponse[ParameterCode.FindFriendsResponseRoomIdList] as string[];
1319
1320 if (onlineList != null && roomList != null && this.friendListRequested != null && onlineList.Length == this.friendListRequested.Length)
1321 {
1322 List<FriendInfo> friendList = new List<FriendInfo>(this.friendListRequested.Length);
1323 for (int index = 0; index < this.friendListRequested.Length; index++)
1324 {
1325 FriendInfo friend = new FriendInfo();
1326 friend.Name = this.friendListRequested[index];
1327 friend.Room = roomList[index];
1328 friend.IsOnline = onlineList[index];
1329 friendList.Insert(index, friend);
1330 }
1331 PhotonNetwork.Friends = friendList;
1332 }
1333 else
1334 {
1335 // any of the lists is null and shouldn't. print a error
1336 Debug.LogError("FindFriends failed to apply the result, as a required value wasn't provided or the friend list length differed from result.");
1337 }
1338
1339 this.friendListRequested = null;
1340 this.isFetchingFriends = false;
1341 this.friendListTimestamp = Environment.TickCount;
1342 if (this.friendListTimestamp == 0)
1343 {
1344 this.friendListTimestamp = 1; // makes sure the timestamp is not accidentally 0
1345 }
1346
1347 SendMonoMessage(PhotonNetworkingMessage.OnUpdatedFriendList);
1348 break;
1349
1350 case OperationCode.WebRpc:
1351 SendMonoMessage(PhotonNetworkingMessage.OnWebRpcResponse, operationResponse);
1352 break;
1353
1354 default:
1355 Debug.LogWarning(string.Format("OperationResponse unhandled: {0}", operationResponse.ToString()));
1356 break;
1357 }
1358
1359 this.externalListener.OnOperationResponse(operationResponse);
1360 }
1361
1362
1363 /// <summary>Contains the list of names of friends to look up their state on the server.</summary>
1364 private string[] friendListRequested;
1365
1366 /// <summary>
1367 /// Age of friend list info (in milliseconds). It's 0 until a friend list is fetched.
1368 /// </summary>
1369 protected internal int FriendsListAge { get { return (this.isFetchingFriends || this.friendListTimestamp == 0) ? 0 : Environment.TickCount - this.friendListTimestamp; } }
1370
1371 private int friendListTimestamp;
1372
1373 /// <summary>Internal flag to know if the client currently fetches a friend list.</summary>
1374 private bool isFetchingFriends;
1375
1376 /// <summary>
1377 /// Request the rooms and online status for a list of friends. All client must set a unique username via PlayerName property. The result is available in this.Friends.
1378 /// </summary>
1379 /// <remarks>
1380 /// Used on Master Server to find the rooms played by a selected list of users.
1381 /// The result will be mapped to LoadBalancingClient.Friends when available.
1382 /// The list is initialized by OpFindFriends on first use (before that, it is null).
1383 ///
1384 /// Users identify themselves by setting a PlayerName in the LoadBalancingClient instance.
1385 /// This in turn will send the name in OpAuthenticate after each connect (to master and game servers).
1386 /// Note: Changing a player's name doesn't make sense when using a friend list.
1387 ///
1388 /// The list of usernames must be fetched from some other source (not provided by Photon).
1389 ///
1390 ///
1391 /// Internal:
1392 /// The server response includes 2 arrays of info (each index matching a friend from the request):
1393 /// ParameterCode.FindFriendsResponseOnlineList = bool[] of online states
1394 /// ParameterCode.FindFriendsResponseRoomIdList = string[] of room names (empty string if not in a room)
1395 /// </remarks>
1396 /// <param name="friendsToFind">Array of friend's names (make sure they are unique).</param>
1397 /// <returns>If the operation could be sent (requires connection, only one request is allowed at any time). Always false in offline mode.</returns>
1398 public override bool OpFindFriends(string[] friendsToFind)
1399 {
1400 if (this.isFetchingFriends)
1401 {
1402 return false; // fetching friends currently, so don't do it again (avoid changing the list while fetching friends)
1403 }
1404
1405 this.friendListRequested = friendsToFind;
1406 this.isFetchingFriends = true;
1407
1408 return base.OpFindFriends(friendsToFind);
1409 }
1410
1411 public void OnStatusChanged(StatusCode statusCode)
1412 {
1413 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
1414 Debug.Log(string.Format("OnStatusChanged: {0}", statusCode.ToString()));
1415
1416 switch (statusCode)
1417 {
1418 case StatusCode.Connect:
1419 if (this.State == global::PeerState.ConnectingToNameServer)
1420 {
1421 if (PhotonNetwork.logLevel >= PhotonLogLevel.Full)
1422 Debug.Log("Connected to NameServer.");
1423
1424 this.server = ServerConnection.NameServer;
1425 if (this.CustomAuthenticationValues != null)
1426 {
1427 this.CustomAuthenticationValues.Secret = null; // when connecting to NameServer, invalidate any auth values
1428 }
1429 }
1430
1431 if (this.State == global::PeerState.ConnectingToGameserver)
1432 {
1433 if (PhotonNetwork.logLevel >= PhotonLogLevel.Full)
1434 Debug.Log("Connected to gameserver.");
1435
1436 this.server = ServerConnection.GameServer;
1437 this.State = global::PeerState.ConnectedToGameserver;
1438 }
1439
1440 if (this.State == global::PeerState.ConnectingToMasterserver)
1441 {
1442 if (PhotonNetwork.logLevel >= PhotonLogLevel.Full)
1443 Debug.Log("Connected to masterserver.");
1444
1445 this.server = ServerConnection.MasterServer;
1446 this.State = global::PeerState.ConnectedToMaster;
1447
1448 if (this.IsInitialConnect)
1449 {
1450 this.IsInitialConnect = false; // after handling potential initial-connect issues with special messages, we are now sure we can reach a server
1451 SendMonoMessage(PhotonNetworkingMessage.OnConnectedToPhoton);
1452 }
1453 }
1454
1455 this.EstablishEncryption(); // always enable encryption
1456
1457 if (this.IsAuthorizeSecretAvailable)
1458 {
1459 // if we have a token we don't have to wait for encryption (it is encrypted anyways, so encryption is just optional later on)
1460 this.didAuthenticate = this.OpAuthenticate(this.mAppId, this.mAppVersionPun, this.PlayerName, this.CustomAuthenticationValues, this.CloudRegion.ToString());
1461 if (this.didAuthenticate)
1462 {
1463 this.State = global::PeerState.Authenticating;
1464 }
1465 }
1466 break;
1467
1468 case StatusCode.EncryptionEstablished:
1469 // on nameserver, the "process" is stopped here, so the developer/game can either get regions or authenticate with a specific region
1470 if (this.server == ServerConnection.NameServer)
1471 {
1472 this.State = global::PeerState.ConnectedToNameServer;
1473
1474 if (!this.didAuthenticate && this.CloudRegion == CloudRegionCode.none)
1475 {
1476 // this client is not setup to connect to a default region. find out which regions there are!
1477 this.OpGetRegions(this.mAppId);
1478 }
1479 }
1480
1481 // we might need to authenticate automatically now, so the client can do anything at all
1482 if (!this.didAuthenticate && (!this.IsUsingNameServer || this.CloudRegion != CloudRegionCode.none))
1483 {
1484 // once encryption is availble, the client should send one (secure) authenticate. it includes the AppId (which identifies your app on the Photon Cloud)
1485 this.didAuthenticate = this.OpAuthenticate(this.mAppId, this.mAppVersionPun, this.PlayerName, this.CustomAuthenticationValues, this.CloudRegion.ToString());
1486 if (this.didAuthenticate)
1487 {
1488 this.State = global::PeerState.Authenticating;
1489 }
1490 }
1491 break;
1492
1493 case StatusCode.EncryptionFailedToEstablish:
1494 Debug.LogError("Encryption wasn't established: " + statusCode + ". Going to authenticate anyways.");
1495 this.OpAuthenticate(this.mAppId, this.mAppVersionPun, this.PlayerName, this.CustomAuthenticationValues, this.CloudRegion.ToString()); // TODO: check if there are alternatives
1496 break;
1497
1498 case StatusCode.Disconnect:
1499 this.didAuthenticate = false;
1500 this.isFetchingFriends = false;
1501 if (server == ServerConnection.GameServer) this.LeftRoomCleanup();
1502 if (server == ServerConnection.MasterServer) this.LeftLobbyCleanup();
1503
1504 if (this.State == global::PeerState.DisconnectingFromMasterserver)
1505 {
1506 if (this.Connect(this.mGameserver, ServerConnection.GameServer))
1507 {
1508 this.State = global::PeerState.ConnectingToGameserver;
1509 }
1510 }
1511 else if (this.State == global::PeerState.DisconnectingFromGameserver || this.State == global::PeerState.DisconnectingFromNameServer)
1512 {
1513 if (this.Connect(this.MasterServerAddress, ServerConnection.MasterServer))
1514 {
1515 this.State = global::PeerState.ConnectingToMasterserver;
1516 }
1517 }
1518 else
1519 {
1520 if (this.CustomAuthenticationValues != null)
1521 {
1522 this.CustomAuthenticationValues.Secret = null; // invalidate any custom auth secrets
1523 }
1524
1525 this.State = global::PeerState.PeerCreated; // if we set another state here, we could keep clients from connecting in OnDisconnectedFromPhoton right here.
1526 SendMonoMessage(PhotonNetworkingMessage.OnDisconnectedFromPhoton);
1527 }
1528 break;
1529
1530 case StatusCode.SecurityExceptionOnConnect:
1531 case StatusCode.ExceptionOnConnect:
1532 this.State = global::PeerState.PeerCreated;
1533 if (this.CustomAuthenticationValues != null)
1534 {
1535 this.CustomAuthenticationValues.Secret = null; // invalidate any custom auth secrets
1536 }
1537
1538 DisconnectCause cause = (DisconnectCause)statusCode;
1539 SendMonoMessage(PhotonNetworkingMessage.OnFailedToConnectToPhoton, cause);
1540 break;
1541
1542 case StatusCode.Exception:
1543 if (this.IsInitialConnect)
1544 {
1545 Debug.LogError("Exception while connecting to: " + this.ServerAddress + ". Check if the server is available.");
1546 if (this.ServerAddress == null || this.ServerAddress.StartsWith("127.0.0.1"))
1547 {
1548 Debug.LogWarning("The server address is 127.0.0.1 (localhost): Make sure the server is running on this machine. Android and iOS emulators have their own localhost.");
1549 if (this.ServerAddress == this.mGameserver)
1550 {
1551 Debug.LogWarning("This might be a misconfiguration in the game server config. You need to edit it to a (public) address.");
1552 }
1553 }
1554
1555 this.State = global::PeerState.PeerCreated;
1556 cause = (DisconnectCause)statusCode;
1557 SendMonoMessage(PhotonNetworkingMessage.OnFailedToConnectToPhoton, cause);
1558 }
1559 else
1560 {
1561 this.State = global::PeerState.PeerCreated;
1562
1563 cause = (DisconnectCause)statusCode;
1564 SendMonoMessage(PhotonNetworkingMessage.OnConnectionFail, cause);
1565 }
1566
1567 this.Disconnect();
1568 break;
1569
1570 case StatusCode.TimeoutDisconnect:
1571 case StatusCode.ExceptionOnReceive:
1572 case StatusCode.DisconnectByServer:
1573 case StatusCode.DisconnectByServerLogic:
1574 case StatusCode.DisconnectByServerUserLimit:
1575 if (this.IsInitialConnect)
1576 {
1577 Debug.LogWarning(statusCode + " while connecting to: " + this.ServerAddress + ". Check if the server is available.");
1578
1579 cause = (DisconnectCause)statusCode;
1580 SendMonoMessage(PhotonNetworkingMessage.OnFailedToConnectToPhoton, cause);
1581 }
1582 else
1583 {
1584 cause = (DisconnectCause)statusCode;
1585 SendMonoMessage(PhotonNetworkingMessage.OnConnectionFail, cause);
1586 }
1587 if (this.CustomAuthenticationValues != null)
1588 {
1589 this.CustomAuthenticationValues.Secret = null; // invalidate any custom auth secrets
1590 }
1591
1592 this.Disconnect();
1593 break;
1594
1595 case StatusCode.SendError:
1596 // this.mListener.clientErrorReturn(statusCode);
1597 break;
1598
1599 case StatusCode.QueueOutgoingReliableWarning:
1600 case StatusCode.QueueOutgoingUnreliableWarning:
1601 case StatusCode.QueueOutgoingAcksWarning:
1602 case StatusCode.QueueSentWarning:
1603 // this.mListener.warningReturn(statusCode);
1604 break;
1605
1606 case StatusCode.QueueIncomingReliableWarning:
1607 case StatusCode.QueueIncomingUnreliableWarning:
1608 Debug.Log(statusCode + ". This client buffers many incoming messages. This is OK temporarily. With lots of these warnings, check if you send too much or execute messages too slow. " + (PhotonNetwork.isMessageQueueRunning? "":"Your isMessageQueueRunning is false. This can cause the issue temporarily.") );
1609 break;
1610
1611 // // TCP "routing" is an option of Photon that's not currently needed (or supported) by PUN
1612 //case StatusCode.TcpRouterResponseOk:
1613 // break;
1614 //case StatusCode.TcpRouterResponseEndpointUnknown:
1615 //case StatusCode.TcpRouterResponseNodeIdUnknown:
1616 //case StatusCode.TcpRouterResponseNodeNotReady:
1617
1618 // this.DebugReturn(DebugLevel.ERROR, "Unexpected router response: " + statusCode);
1619 // break;
1620
1621 default:
1622
1623 // this.mListener.serverErrorReturn(statusCode.value());
1624 Debug.LogError("Received unknown status code: " + statusCode);
1625 break;
1626 }
1627
1628 this.externalListener.OnStatusChanged(statusCode);
1629 }
1630
1631 public void OnEvent(EventData photonEvent)
1632 {
1633 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
1634 Debug.Log(string.Format("OnEvent: {0}", photonEvent.ToString()));
1635
1636 int actorNr = -1;
1637 PhotonPlayer originatingPlayer = null;
1638
1639 if (photonEvent.Parameters.ContainsKey(ParameterCode.ActorNr))
1640 {
1641 actorNr = (int)photonEvent[ParameterCode.ActorNr];
1642 if (this.mActors.ContainsKey(actorNr))
1643 {
1644 originatingPlayer = (PhotonPlayer)this.mActors[actorNr];
1645 }
1646 //else
1647 //{
1648 // // the actor sending this event is not in actorlist. this is usually no problem
1649 // if (photonEvent.Code != (byte)LiteOpCode.Join)
1650 // {
1651 // Debug.LogWarning("Received event, but we do not have this actor: " + actorNr);
1652 // }
1653 //}
1654 }
1655
1656 switch (photonEvent.Code)
1657 {
1658 case PunEvent.OwnershipRequest:
1659 {
1660 int[] requestValues = (int[]) photonEvent.Parameters[ParameterCode.CustomEventContent];
1661 int requestedViewId = requestValues[0];
1662 int currentOwner = requestValues[1];
1663 Debug.Log("Ev OwnershipRequest: " + photonEvent.Parameters.ToStringFull() + " ViewID: " + requestedViewId + " from: " + currentOwner + " Time: " + Environment.TickCount%1000);
1664
1665 PhotonView requestedView = PhotonView.Find(requestedViewId);
1666 if (requestedView == null)
1667 {
1668 Debug.LogWarning("Can't find PhotonView of incoming OwnershipRequest. ViewId not found: " + requestedViewId);
1669 break;
1670 }
1671
1672 Debug.Log("Ev OwnershipRequest PhotonView.ownershipTransfer: " + requestedView.ownershipTransfer + " .ownerId: " + requestedView.ownerId + " isOwnerActive: " + requestedView.isOwnerActive + ". This client's player: " + PhotonNetwork.player.ToStringFull());
1673
1674 switch (requestedView.ownershipTransfer)
1675 {
1676 case OwnershipOption.Fixed:
1677 Debug.LogWarning("Ownership mode == fixed. Ignoring request.");
1678 break;
1679 case OwnershipOption.Takeover:
1680 if (currentOwner == requestedView.ownerId)
1681 {
1682 // a takeover is successful automatically, if taken from current owner
1683 requestedView.ownerId = actorNr;
1684 }
1685 break;
1686 case OwnershipOption.Request:
1687 if (currentOwner == PhotonNetwork.player.ID || PhotonNetwork.player.isMasterClient)
1688 {
1689 if ((requestedView.ownerId == PhotonNetwork.player.ID) || (PhotonNetwork.player.isMasterClient && !requestedView.isOwnerActive))
1690 {
1691 SendMonoMessage(PhotonNetworkingMessage.OnOwnershipRequest, new object[] {requestedView, originatingPlayer});
1692 }
1693 }
1694 break;
1695 default:
1696 break;
1697 }
1698 }
1699 break;
1700
1701 case PunEvent.OwnershipTransfer:
1702 {
1703 int[] transferViewToUserID = (int[]) photonEvent.Parameters[ParameterCode.CustomEventContent];
1704 Debug.Log("Ev OwnershipTransfer. ViewID " + transferViewToUserID[0] + " to: " + transferViewToUserID[1] + " Time: " + Environment.TickCount%1000);
1705
1706 int requestedViewId = transferViewToUserID[0];
1707 int newOwnerId = transferViewToUserID[1];
1708
1709 PhotonView pv = PhotonView.Find(requestedViewId);
1710 pv.ownerId = newOwnerId;
1711
1712 break;
1713 }
1714 case EventCode.GameList:
1715 {
1716 this.mGameList = new Dictionary<string, RoomInfo>();
1717 Hashtable games = (Hashtable)photonEvent[ParameterCode.GameList];
1718 foreach (DictionaryEntry game in games)
1719 {
1720 string gameName = (string)game.Key;
1721 this.mGameList[gameName] = new RoomInfo(gameName, (Hashtable)game.Value);
1722 }
1723 mGameListCopy = new RoomInfo[mGameList.Count];
1724 mGameList.Values.CopyTo(mGameListCopy, 0);
1725 SendMonoMessage(PhotonNetworkingMessage.OnReceivedRoomListUpdate);
1726 break;
1727 }
1728
1729 case EventCode.GameListUpdate:
1730 {
1731 Hashtable games = (Hashtable)photonEvent[ParameterCode.GameList];
1732 foreach (DictionaryEntry room in games)
1733 {
1734 string gameName = (string)room.Key;
1735 RoomInfo game = new RoomInfo(gameName, (Hashtable)room.Value);
1736 if (game.removedFromList)
1737 {
1738 this.mGameList.Remove(gameName);
1739 }
1740 else
1741 {
1742 this.mGameList[gameName] = game;
1743 }
1744 }
1745 this.mGameListCopy = new RoomInfo[this.mGameList.Count];
1746 this.mGameList.Values.CopyTo(this.mGameListCopy, 0);
1747 SendMonoMessage(PhotonNetworkingMessage.OnReceivedRoomListUpdate);
1748 break;
1749 }
1750
1751 case EventCode.QueueState:
1752 // not used anymore
1753 break;
1754
1755 case EventCode.AppStats:
1756 // Debug.LogInfo("Received stats!");
1757 this.mPlayersInRoomsCount = (int)photonEvent[ParameterCode.PeerCount];
1758 this.mPlayersOnMasterCount = (int)photonEvent[ParameterCode.MasterPeerCount];
1759 this.mGameCount = (int)photonEvent[ParameterCode.GameCount];
1760 break;
1761
1762 case EventCode.Join:
1763 // actorNr is fetched out of event above
1764 Hashtable actorProperties = (Hashtable)photonEvent[ParameterCode.PlayerProperties];
1765 if (originatingPlayer == null)
1766 {
1767 bool isLocal = this.mLocalActor.ID == actorNr;
1768 this.AddNewPlayer(actorNr, new PhotonPlayer(isLocal, actorNr, actorProperties));
1769 this.ResetPhotonViewsOnSerialize(); // This sets the correct OnSerializeState for Reliable OnSerialize
1770 }
1771
1772 if (actorNr == this.mLocalActor.ID)
1773 {
1774 // in this player's 'own' join event, we get a complete list of players in the room, so check if we know all players
1775 int[] actorsInRoom = (int[])photonEvent[ParameterCode.ActorList];
1776 foreach (int actorNrToCheck in actorsInRoom)
1777 {
1778 if (this.mLocalActor.ID != actorNrToCheck && !this.mActors.ContainsKey(actorNrToCheck))
1779 {
1780 this.AddNewPlayer(actorNrToCheck, new PhotonPlayer(false, actorNrToCheck, string.Empty));
1781 }
1782 }
1783
1784 // joinWithCreateOnDemand can turn an OpJoin into creating the room. Then actorNumber is 1 and callback: OnCreatedRoom()
1785 if (this.mLastJoinType == JoinType.JoinOrCreateOnDemand && this.mLocalActor.ID == 1)
1786 {
1787 SendMonoMessage(PhotonNetworkingMessage.OnCreatedRoom);
1788 }
1789 SendMonoMessage(PhotonNetworkingMessage.OnJoinedRoom); //Always send OnJoinedRoom
1790
1791 }
1792 else
1793 {
1794 SendMonoMessage(PhotonNetworkingMessage.OnPhotonPlayerConnected, this.mActors[actorNr]);
1795 }
1796 break;
1797
1798 case EventCode.Leave:
1799 this.HandleEventLeave(actorNr);
1800 break;
1801
1802 case EventCode.PropertiesChanged:
1803 int targetActorNr = (int)photonEvent[ParameterCode.TargetActorNr];
1804 Hashtable gameProperties = null;
1805 Hashtable actorProps = null;
1806 if (targetActorNr == 0)
1807 {
1808 gameProperties = (Hashtable)photonEvent[ParameterCode.Properties];
1809 }
1810 else
1811 {
1812 actorProps = (Hashtable)photonEvent[ParameterCode.Properties];
1813 }
1814
1815 this.ReadoutProperties(gameProperties, actorProps, targetActorNr);
1816 break;
1817
1818 case PunEvent.RPC:
1819 //ts: each event now contains a single RPC. execute this
1820 // Debug.Log("Ev RPC from: " + originatingPlayer);
1821 this.ExecuteRPC(photonEvent[ParameterCode.Data] as Hashtable, originatingPlayer);
1822 break;
1823
1824 case PunEvent.SendSerialize:
1825 case PunEvent.SendSerializeReliable:
1826 Hashtable serializeData = (Hashtable)photonEvent[ParameterCode.Data];
1827 //Debug.Log(serializeData.ToStringFull());
1828
1829 int remoteUpdateServerTimestamp = (int)serializeData[(byte)0];
1830 short remoteLevelPrefix = -1;
1831 short initialDataIndex = 1;
1832 if (serializeData.ContainsKey((byte)1))
1833 {
1834 remoteLevelPrefix = (short)serializeData[(byte)1];
1835 initialDataIndex = 2;
1836 }
1837
1838 for (short s = initialDataIndex; s < serializeData.Count; s++)
1839 {
1840 this.OnSerializeRead(serializeData[s] as Hashtable, originatingPlayer, remoteUpdateServerTimestamp, remoteLevelPrefix);
1841 }
1842 break;
1843
1844 case PunEvent.Instantiation:
1845 this.DoInstantiate((Hashtable)photonEvent[ParameterCode.Data], originatingPlayer, null);
1846 break;
1847
1848 case PunEvent.CloseConnection:
1849 // MasterClient "requests" a disconnection from us
1850 if (originatingPlayer == null || !originatingPlayer.isMasterClient)
1851 {
1852 Debug.LogError("Error: Someone else(" + originatingPlayer + ") then the masterserver requests a disconnect!");
1853 }
1854 else
1855 {
1856 PhotonNetwork.LeaveRoom();
1857 }
1858
1859 break;
1860
1861 case PunEvent.DestroyPlayer:
1862 Hashtable evData = (Hashtable)photonEvent[ParameterCode.Data];
1863 int targetPlayerId = (int)evData[(byte)0];
1864 if (targetPlayerId >= 0)
1865 {
1866 this.DestroyPlayerObjects(targetPlayerId, true);
1867 }
1868 else
1869 {
1870 if (this.DebugOut >= DebugLevel.INFO) Debug.Log("Ev DestroyAll! By PlayerId: " + actorNr);
1871 this.DestroyAll(true);
1872 }
1873 break;
1874
1875 case PunEvent.Destroy:
1876 evData = (Hashtable)photonEvent[ParameterCode.Data];
1877 int instantiationId = (int)evData[(byte)0];
1878 // Debug.Log("Ev Destroy for viewId: " + instantiationId + " sent by owner: " + (instantiationId / PhotonNetwork.MAX_VIEW_IDS == actorNr) + " this client is owner: " + (instantiationId / PhotonNetwork.MAX_VIEW_IDS == this.mLocalActor.ID));
1879
1880
1881 PhotonView pvToDestroy = null;
1882 if (this.photonViewList.TryGetValue(instantiationId, out pvToDestroy))
1883 {
1884 this.RemoveInstantiatedGO(pvToDestroy.gameObject, true);
1885 }
1886 else
1887 {
1888 if (this.DebugOut >= DebugLevel.ERROR) Debug.LogError("Ev Destroy Failed. Could not find PhotonView with instantiationId " + instantiationId + ". Sent by actorNr: " + actorNr);
1889 }
1890
1891 break;
1892
1893 case PunEvent.AssignMaster:
1894 evData = (Hashtable)photonEvent[ParameterCode.Data];
1895 int newMaster = (int)evData[(byte)1];
1896 this.SetMasterClient(newMaster, false);
1897 break;
1898
1899 default:
1900 if (photonEvent.Code < 200 && PhotonNetwork.OnEventCall != null)
1901 {
1902 object content = photonEvent[ParameterCode.Data];
1903 PhotonNetwork.OnEventCall(photonEvent.Code, content, actorNr);
1904 }
1905 else
1906 {
1907 // actorNr might be null. it is fetched out of event on top of method
1908 // Hashtable eventContent = (Hashtable) photonEvent[ParameterCode.Data];
1909 // this.mListener.customEventAction(actorNr, eventCode, eventContent);
1910 Debug.LogError("Error. Unhandled event: " + photonEvent);
1911 }
1912 break;
1913 }
1914
1915 this.externalListener.OnEvent(photonEvent);
1916 }
1917
1918 private void SendVacantViewIds()
1919 {
1920 Debug.Log("SendVacantViewIds()");
1921 List<int> vacantViews = new List<int>();
1922 foreach (PhotonView view in this.photonViewList.Values)
1923 {
1924 if (!view.isOwnerActive)
1925 {
1926 vacantViews.Add(view.viewID);
1927 }
1928 }
1929
1930 Debug.Log("Sending vacant view IDs. Length: " + vacantViews.Count);
1931 //this.OpRaiseEvent(PunEvent.VacantViewIds, true, vacantViews.ToArray());
1932 this.OpRaiseEvent(PunEvent.VacantViewIds, vacantViews.ToArray(), true, null);
1933 }
1934
1935 #endregion
1936
1937 public static void SendMonoMessage(PhotonNetworkingMessage methodString, params object[] parameters)
1938 {
1939 HashSet<GameObject> objectsToCall;
1940 if (PhotonNetwork.SendMonoMessageTargets != null)
1941 {
1942 objectsToCall = PhotonNetwork.SendMonoMessageTargets;
1943 }
1944 else
1945 {
1946 objectsToCall = PhotonNetwork.FindGameObjectsWithComponent(PhotonNetwork.SendMonoMessageTargetType);
1947 }
1948
1949 string methodName = methodString.ToString();
1950 object callParameter = (parameters != null && parameters.Length == 1) ? parameters[0] : parameters;
1951 foreach (GameObject gameObject in objectsToCall)
1952 {
1953 gameObject.SendMessage(methodName, callParameter, SendMessageOptions.DontRequireReceiver);
1954 }
1955 }
1956
1957 // PHOTONVIEW/RPC related
1958
1959 /// <summary>
1960 /// Executes a received RPC event
1961 /// </summary>
1962 public void ExecuteRPC(Hashtable rpcData, PhotonPlayer sender)
1963 {
1964 if (rpcData == null || !rpcData.ContainsKey((byte)0))
1965 {
1966 Debug.LogError("Malformed RPC; this should never occur. Content: " + SupportClass.DictionaryToString(rpcData));
1967 return;
1968 }
1969
1970 // ts: updated with "flat" event data
1971 int netViewID = (int)rpcData[(byte)0]; // LIMITS PHOTONVIEWS&PLAYERS
1972 int otherSidePrefix = 0; // by default, the prefix is 0 (and this is not being sent)
1973 if (rpcData.ContainsKey((byte)1))
1974 {
1975 otherSidePrefix = (short)rpcData[(byte)1];
1976 }
1977
1978 string inMethodName;
1979 if (rpcData.ContainsKey((byte)5))
1980 {
1981 int rpcIndex = (byte)rpcData[(byte)5]; // LIMITS RPC COUNT
1982 if (rpcIndex > PhotonNetwork.PhotonServerSettings.RpcList.Count - 1)
1983 {
1984 Debug.LogError("Could not find RPC with index: " + rpcIndex + ". Going to ignore! Check PhotonServerSettings.RpcList");
1985 return;
1986 }
1987 else
1988 {
1989 inMethodName = PhotonNetwork.PhotonServerSettings.RpcList[rpcIndex];
1990 }
1991 }
1992 else
1993 {
1994 inMethodName = (string)rpcData[(byte)3];
1995 }
1996
1997 object[] inMethodParameters = null;
1998 if (rpcData.ContainsKey((byte)4))
1999 {
2000 inMethodParameters = (object[])rpcData[(byte)4];
2001 }
2002
2003 if (inMethodParameters == null)
2004 {
2005 inMethodParameters = new object[0];
2006 }
2007
2008 PhotonView photonNetview = this.GetPhotonView(netViewID);
2009 if (photonNetview == null)
2010 {
2011 int viewOwnerId = netViewID/PhotonNetwork.MAX_VIEW_IDS;
2012 bool owningPv = (viewOwnerId == this.mLocalActor.ID);
2013 bool ownerSent = (viewOwnerId == sender.ID);
2014
2015 if (owningPv)
2016 {
2017 Debug.LogWarning("Received RPC \"" + inMethodName + "\" for viewID " + netViewID + " but this PhotonView does not exist! View was/is ours." + (ownerSent ? " Owner called." : " Remote called.") + " By: " + sender.ID);
2018 }
2019 else
2020 {
2021 Debug.LogWarning("Received RPC \"" + inMethodName + "\" for viewID " + netViewID + " but this PhotonView does not exist! Was remote PV." + (ownerSent ? " Owner called." : " Remote called.") + " By: " + sender.ID + " Maybe GO was destroyed but RPC not cleaned up.");
2022 }
2023 return;
2024 }
2025
2026 if (photonNetview.prefix != otherSidePrefix)
2027 {
2028 Debug.LogError(
2029 "Received RPC \"" + inMethodName + "\" on viewID " + netViewID + " with a prefix of " + otherSidePrefix
2030 + ", our prefix is " + photonNetview.prefix + ". The RPC has been ignored.");
2031 return;
2032 }
2033
2034 // Get method name
2035 if (inMethodName == string.Empty)
2036 {
2037 Debug.LogError("Malformed RPC; this should never occur. Content: " + SupportClass.DictionaryToString(rpcData));
2038 return;
2039 }
2040
2041 if (PhotonNetwork.logLevel >= PhotonLogLevel.Full)
2042 Debug.Log("Received RPC: " + inMethodName);
2043
2044
2045 // SetReceiving filtering
2046 if (photonNetview.group != 0 && !allowedReceivingGroups.Contains(photonNetview.group))
2047 {
2048 return; // Ignore group
2049 }
2050
2051 Type[] argTypes = new Type[0];
2052 if (inMethodParameters.Length > 0)
2053 {
2054 argTypes = new Type[inMethodParameters.Length];
2055 int i = 0;
2056 for (int index = 0; index < inMethodParameters.Length; index++)
2057 {
2058 object objX = inMethodParameters[index];
2059 if (objX == null)
2060 {
2061 argTypes[i] = null;
2062 }
2063 else
2064 {
2065 argTypes[i] = objX.GetType();
2066 }
2067
2068 i++;
2069 }
2070 }
2071
2072 int receivers = 0;
2073 int foundMethods = 0;
2074 MonoBehaviour[] mbComponents = photonNetview.GetComponents<MonoBehaviour>(); // NOTE: we could possibly also cache MonoBehaviours per view?!
2075 for (int componentsIndex = 0; componentsIndex < mbComponents.Length; componentsIndex++)
2076 {
2077 MonoBehaviour monob = mbComponents[componentsIndex];
2078 if (monob == null)
2079 {
2080 Debug.LogError("ERROR You have missing MonoBehaviours on your gameobjects!");
2081 continue;
2082 }
2083
2084 Type type = monob.GetType();
2085
2086 // Get [RPC] methods from cache
2087 List<MethodInfo> cachedRPCMethods = null;
2088 if (this.monoRPCMethodsCache.ContainsKey(type))
2089 {
2090 cachedRPCMethods = this.monoRPCMethodsCache[type];
2091 }
2092
2093 if (cachedRPCMethods == null)
2094 {
2095 List<MethodInfo> entries = SupportClass.GetMethods(type, typeof(RPC));
2096
2097 this.monoRPCMethodsCache[type] = entries;
2098 cachedRPCMethods = entries;
2099 }
2100
2101 if (cachedRPCMethods == null)
2102 {
2103 continue;
2104 }
2105
2106 // Check cache for valid methodname+arguments
2107 for (int index = 0; index < cachedRPCMethods.Count; index++)
2108 {
2109 MethodInfo mInfo = cachedRPCMethods[index];
2110 if (mInfo.Name == inMethodName)
2111 {
2112 foundMethods++;
2113 ParameterInfo[] pArray = mInfo.GetParameters();
2114 if (pArray.Length == argTypes.Length)
2115 {
2116 // Normal, PhotonNetworkMessage left out
2117 if (this.CheckTypeMatch(pArray, argTypes))
2118 {
2119 receivers++;
2120 object result = mInfo.Invoke((object)monob, inMethodParameters);
2121 if (mInfo.ReturnType == typeof(IEnumerator))
2122 {
2123 monob.StartCoroutine((IEnumerator)result);
2124 }
2125 }
2126 }
2127 else if ((pArray.Length - 1) == argTypes.Length)
2128 {
2129 // Check for PhotonNetworkMessage being the last
2130 if (this.CheckTypeMatch(pArray, argTypes))
2131 {
2132 if (pArray[pArray.Length - 1].ParameterType == typeof(PhotonMessageInfo))
2133 {
2134 receivers++;
2135
2136 int sendTime = (int)rpcData[(byte)2];
2137 object[] deParamsWithInfo = new object[inMethodParameters.Length + 1];
2138 inMethodParameters.CopyTo(deParamsWithInfo, 0);
2139 deParamsWithInfo[deParamsWithInfo.Length - 1] = new PhotonMessageInfo(sender, sendTime, photonNetview);
2140
2141 object result = mInfo.Invoke((object)monob, deParamsWithInfo);
2142 if (mInfo.ReturnType == typeof(IEnumerator))
2143 {
2144 monob.StartCoroutine((IEnumerator)result);
2145 }
2146 }
2147 }
2148 }
2149 else if (pArray.Length == 1 && pArray[0].ParameterType.IsArray)
2150 {
2151 receivers++;
2152 object result = mInfo.Invoke((object)monob, new object[] { inMethodParameters });
2153 if (mInfo.ReturnType == typeof(IEnumerator))
2154 {
2155 monob.StartCoroutine((IEnumerator)result);
2156 }
2157 }
2158 }
2159 }
2160 }
2161
2162 // Error handling
2163 if (receivers != 1)
2164 {
2165 string argsString = string.Empty;
2166 for (int index = 0; index < argTypes.Length; index++)
2167 {
2168 Type ty = argTypes[index];
2169 if (argsString != string.Empty)
2170 {
2171 argsString += ", ";
2172 }
2173
2174 if (ty == null)
2175 {
2176 argsString += "null";
2177 }
2178 else
2179 {
2180 argsString += ty.Name;
2181 }
2182 }
2183
2184 if (receivers == 0)
2185 {
2186 if (foundMethods == 0)
2187 {
2188 Debug.LogError("PhotonView with ID " + netViewID + " has no method \"" + inMethodName + "\" marked with the [RPC](C#) or @RPC(JS) property! Args: " + argsString);
2189 }
2190 else
2191 {
2192 Debug.LogError("PhotonView with ID " + netViewID + " has no method \"" + inMethodName + "\" that takes " + argTypes.Length + " argument(s): " + argsString);
2193 }
2194 }
2195 else
2196 {
2197 Debug.LogError("PhotonView with ID " + netViewID + " has " + receivers + " methods \"" + inMethodName + "\" that takes " + argTypes.Length + " argument(s): " + argsString + ". Should be just one?");
2198 }
2199 }
2200 }
2201
2202 /// <summary>
2203 /// Check if all types match with parameters. We can have more paramters then types (allow last RPC type to be different).
2204 /// </summary>
2205 /// <param name="methodParameters"></param>
2206 /// <param name="callParameterTypes"></param>
2207 /// <returns>If the types-array has matching parameters (of method) in the parameters array (which may be longer).</returns>
2208 private bool CheckTypeMatch(ParameterInfo[] methodParameters, Type[] callParameterTypes)
2209 {
2210 if (methodParameters.Length < callParameterTypes.Length)
2211 {
2212 return false;
2213 }
2214
2215 for (int index = 0; index < callParameterTypes.Length; index++)
2216 {
2217 Type type = methodParameters[index].ParameterType;
2218 //todo: check metro type usage
2219 if (callParameterTypes[index] != null && !type.Equals(callParameterTypes[index]))
2220 {
2221 return false;
2222 }
2223 }
2224
2225 return true;
2226 }
2227
2228 internal Hashtable SendInstantiate(string prefabName, Vector3 position, Quaternion rotation, int group, int[] viewIDs, object[] data, bool isGlobalObject)
2229 {
2230 // first viewID is now also the gameobject's instantiateId
2231 int instantiateId = viewIDs[0]; // LIMITS PHOTONVIEWS&PLAYERS
2232
2233 //TODO: reduce hashtable key usage by using a parameter array for the various values
2234 Hashtable instantiateEvent = new Hashtable(); // This players info is sent via ActorID
2235 instantiateEvent[(byte)0] = prefabName;
2236
2237 if (position != Vector3.zero)
2238 {
2239 instantiateEvent[(byte)1] = position;
2240 }
2241
2242 if (rotation != Quaternion.identity)
2243 {
2244 instantiateEvent[(byte)2] = rotation;
2245 }
2246
2247 if (group != 0)
2248 {
2249 instantiateEvent[(byte)3] = group;
2250 }
2251
2252 // send the list of viewIDs only if there are more than one. else the instantiateId is the viewID
2253 if (viewIDs.Length > 1)
2254 {
2255 instantiateEvent[(byte)4] = viewIDs; // LIMITS PHOTONVIEWS&PLAYERS
2256 }
2257
2258 if (data != null)
2259 {
2260 instantiateEvent[(byte)5] = data;
2261 }
2262
2263 if (this.currentLevelPrefix > 0)
2264 {
2265 instantiateEvent[(byte)8] = this.currentLevelPrefix; // photonview's / object's level prefix
2266 }
2267
2268 instantiateEvent[(byte)6] = this.ServerTimeInMilliSeconds;
2269 instantiateEvent[(byte)7] = instantiateId;
2270
2271
2272 RaiseEventOptions options = new RaiseEventOptions();
2273 options.CachingOption = (isGlobalObject) ? EventCaching.AddToRoomCacheGlobal : EventCaching.AddToRoomCache;
2274
2275 this.OpRaiseEvent(PunEvent.Instantiation, instantiateEvent, true, options);
2276 return instantiateEvent;
2277 }
2278
2279 internal GameObject DoInstantiate(Hashtable evData, PhotonPlayer photonPlayer, GameObject resourceGameObject)
2280 {
2281 // some values always present:
2282 string prefabName = (string)evData[(byte)0];
2283 int serverTime = (int)evData[(byte)6];
2284 int instantiationId = (int)evData[(byte)7];
2285
2286 Vector3 position;
2287 if (evData.ContainsKey((byte)1))
2288 {
2289 position = (Vector3)evData[(byte)1];
2290 }
2291 else
2292 {
2293 position = Vector3.zero;
2294 }
2295
2296 Quaternion rotation = Quaternion.identity;
2297 if (evData.ContainsKey((byte)2))
2298 {
2299 rotation = (Quaternion)evData[(byte)2];
2300 }
2301
2302 int group = 0;
2303 if (evData.ContainsKey((byte)3))
2304 {
2305 group = (int)evData[(byte)3];
2306 }
2307
2308 short objLevelPrefix = 0;
2309 if (evData.ContainsKey((byte)8))
2310 {
2311 objLevelPrefix = (short)evData[(byte)8];
2312 }
2313
2314 int[] viewsIDs;
2315 if (evData.ContainsKey((byte)4))
2316 {
2317 viewsIDs = (int[])evData[(byte)4];
2318 }
2319 else
2320 {
2321 viewsIDs = new int[1] { instantiationId };
2322 }
2323
2324 object[] incomingInstantiationData;
2325 if (evData.ContainsKey((byte)5))
2326 {
2327 incomingInstantiationData = (object[])evData[(byte)5];
2328 }
2329 else
2330 {
2331 incomingInstantiationData = null;
2332 }
2333
2334 // SetReceiving filtering
2335 if (group != 0 && !this.allowedReceivingGroups.Contains(group))
2336 {
2337 return null; // Ignore group
2338 }
2339
2340 // load prefab, if it wasn't loaded before (calling methods might do this)
2341 if (resourceGameObject == null)
2342 {
2343 if (!NetworkingPeer.UsePrefabCache || !NetworkingPeer.PrefabCache.TryGetValue(prefabName, out resourceGameObject))
2344 {
2345 resourceGameObject = (GameObject)Resources.Load(prefabName, typeof(GameObject));
2346 if (NetworkingPeer.UsePrefabCache)
2347 {
2348 NetworkingPeer.PrefabCache.Add(prefabName, resourceGameObject);
2349 }
2350 }
2351
2352 if (resourceGameObject == null)
2353 {
2354 Debug.LogError("PhotonNetwork error: Could not Instantiate the prefab [" + prefabName + "]. Please verify you have this gameobject in a Resources folder.");
2355 return null;
2356 }
2357 }
2358
2359 // now modify the loaded "blueprint" object before it becomes a part of the scene (by instantiating it)
2360 PhotonView[] resourcePVs = resourceGameObject.GetPhotonViewsInChildren();
2361 if (resourcePVs.Length != viewsIDs.Length)
2362 {
2363 throw new Exception("Error in Instantiation! The resource's PhotonView count is not the same as in incoming data.");
2364 }
2365
2366 for (int i = 0; i < viewsIDs.Length; i++)
2367 {
2368 // NOTE instantiating the loaded resource will keep the viewID but would not copy instantiation data, so it's set below
2369 // so we only set the viewID and instantiationId now. the instantiationData can be fetched
2370 resourcePVs[i].viewID = viewsIDs[i];
2371 resourcePVs[i].prefix = objLevelPrefix;
2372 resourcePVs[i].instantiationId = instantiationId;
2373 resourcePVs[i].isRuntimeInstantiated = true;
2374 }
2375
2376 this.StoreInstantiationData(instantiationId, incomingInstantiationData);
2377
2378 // load the resource and set it's values before instantiating it:
2379 GameObject go = (GameObject)GameObject.Instantiate(resourceGameObject, position, rotation);
2380
2381 for (int i = 0; i < viewsIDs.Length; i++)
2382 {
2383 // NOTE instantiating the loaded resource will keep the viewID but would not copy instantiation data, so it's set below
2384 // so we only set the viewID and instantiationId now. the instantiationData can be fetched
2385 resourcePVs[i].viewID = 0;
2386 resourcePVs[i].prefix = -1;
2387 resourcePVs[i].prefixBackup = -1;
2388 resourcePVs[i].instantiationId = -1;
2389 resourcePVs[i].isRuntimeInstantiated = false;
2390 }
2391
2392 this.RemoveInstantiationData(instantiationId);
2393
2394 // Send OnPhotonInstantiate callback to newly created GO.
2395 // GO will be enabled when instantiated from Prefab and it does not matter if the script is enabled or disabled.
2396 go.SendMessage(PhotonNetworkingMessage.OnPhotonInstantiate.ToString(), new PhotonMessageInfo(photonPlayer, serverTime, null), SendMessageOptions.DontRequireReceiver);
2397 return go;
2398 }
2399
2400 private Dictionary<int, object[]> tempInstantiationData = new Dictionary<int, object[]>();
2401
2402 private void StoreInstantiationData(int instantiationId, object[] instantiationData)
2403 {
2404 // Debug.Log("StoreInstantiationData() instantiationId: " + instantiationId + " tempInstantiationData.Count: " + tempInstantiationData.Count);
2405 tempInstantiationData[instantiationId] = instantiationData;
2406 }
2407
2408 public object[] FetchInstantiationData(int instantiationId)
2409 {
2410 object[] data = null;
2411 if (instantiationId == 0)
2412 {
2413 return null;
2414 }
2415
2416 tempInstantiationData.TryGetValue(instantiationId, out data);
2417 // Debug.Log("FetchInstantiationData() instantiationId: " + instantiationId + " tempInstantiationData.Count: " + tempInstantiationData.Count);
2418 return data;
2419 }
2420
2421 private void RemoveInstantiationData(int instantiationId)
2422 {
2423 tempInstantiationData.Remove(instantiationId);
2424 }
2425
2426
2427 /// <summary>
2428 /// Destroys all Instantiates and RPCs locally and (if not localOnly) sends EvDestroy(player) and clears related events in the server buffer.
2429 /// </summary>
2430 public void DestroyPlayerObjects(int playerId, bool localOnly)
2431 {
2432 if (playerId <= 0)
2433 {
2434 Debug.LogError("Failed to Destroy objects of playerId: " + playerId);
2435 return;
2436 }
2437
2438 if (!localOnly)
2439 {
2440 // clean server's Instantiate and RPC buffers
2441 this.OpRemoveFromServerInstantiationsOfPlayer(playerId);
2442 this.OpCleanRpcBuffer(playerId);
2443
2444 // send Destroy(player) to anyone else
2445 this.SendDestroyOfPlayer(playerId);
2446 }
2447
2448 // locally cleaning up that player's objects
2449 HashSet<GameObject> playersGameObjects = new HashSet<GameObject>();
2450 foreach (PhotonView view in this.photonViewList.Values)
2451 {
2452 if (view.CreatorActorNr == playerId)
2453 {
2454 playersGameObjects.Add(view.gameObject);
2455 }
2456 }
2457
2458 // any non-local work is already done, so with the list of that player's objects, we can clean up (locally only)
2459 foreach (GameObject gameObject in playersGameObjects)
2460 {
2461 this.RemoveInstantiatedGO(gameObject, true);
2462 }
2463
2464 // with ownership transfer, some objects might lose their owner.
2465 // in that case, the creator becomes the owner again. every client can apply this. done below.
2466 foreach (PhotonView view in this.photonViewList.Values)
2467 {
2468 if (view.ownerId == playerId)
2469 {
2470 view.ownerId = view.CreatorActorNr;
2471 //Debug.Log("Creator is: " + view.ownerId);
2472 }
2473 }
2474 }
2475
2476 public void DestroyAll(bool localOnly)
2477 {
2478 if (!localOnly)
2479 {
2480 this.OpRemoveCompleteCache();
2481 this.SendDestroyOfAll();
2482 }
2483
2484 this.LocalCleanupAnythingInstantiated(true);
2485 }
2486
2487 /// <summary>Removes GameObject and the PhotonViews on it from local lists and optionally updates remotes. GameObject gets destroyed at end.</summary>
2488 /// <remarks>
2489 /// This method might fail and quit early due to several tests.
2490 /// </remarks>
2491 /// <param name="go">GameObject to cleanup.</param>
2492 /// <param name="localOnly">For localOnly, tests of control are skipped and the server is not updated.</param>
2493 protected internal void RemoveInstantiatedGO(GameObject go, bool localOnly)
2494 {
2495 if (go == null)
2496 {
2497 Debug.LogError("Failed to 'network-remove' GameObject because it's null.");
2498 return;
2499 }
2500
2501 // Don't remove the GO if it doesn't have any PhotonView
2502 PhotonView[] views = go.GetComponentsInChildren<PhotonView>(true);
2503 if (views == null || views.Length <= 0)
2504 {
2505 Debug.LogError("Failed to 'network-remove' GameObject because has no PhotonView components: " + go);
2506 return;
2507 }
2508
2509 PhotonView viewZero = views[0];
2510 int creatorId = viewZero.CreatorActorNr; // creatorId of obj is needed to delete EvInstantiate (only if it's from that user)
2511 int instantiationId = viewZero.instantiationId; // actual, live InstantiationIds start with 1 and go up
2512
2513 // Don't remove GOs that are owned by others (unless this is the master and the remote player left)
2514 if (!localOnly)
2515 {
2516 if (!viewZero.isMine)
2517 {
2518 Debug.LogError("Failed to 'network-remove' GameObject. Client is neither owner nor masterClient taking over for owner who left: " + viewZero);
2519 return;
2520 }
2521
2522 // Don't remove the Instantiation from the server, if it doesn't have a proper ID
2523 if (instantiationId < 1)
2524 {
2525 Debug.LogError("Failed to 'network-remove' GameObject because it is missing a valid InstantiationId on view: " + viewZero + ". Not Destroying GameObject or PhotonViews!");
2526 return;
2527 }
2528 }
2529
2530
2531 // cleanup instantiation (event and local list)
2532 if (!localOnly)
2533 {
2534 this.ServerCleanInstantiateAndDestroy(instantiationId, creatorId, viewZero.isRuntimeInstantiated); // server cleaning
2535 }
2536
2537
2538 // cleanup PhotonViews and their RPCs events (if not localOnly)
2539 for (int j = views.Length - 1; j >= 0; j--)
2540 {
2541 PhotonView view = views[j];
2542 if (view == null)
2543 {
2544 continue;
2545 }
2546
2547 // we only destroy/clean PhotonViews that were created by PhotonNetwork.Instantiate (and those have an instantiationId!)
2548 if (view.instantiationId >= 1)
2549 {
2550 this.LocalCleanPhotonView(view);
2551 }
2552 if (!localOnly)
2553 {
2554 this.OpCleanRpcBuffer(view);
2555 }
2556 }
2557
2558 if (PhotonNetwork.logLevel >= PhotonLogLevel.Full)
2559 Debug.Log("Network destroy Instantiated GO: " + go.name);
2560
2561 GameObject.Destroy(go);
2562 }
2563
2564 /// <summary>
2565 /// This returns -1 if the GO could not be found in list of instantiatedObjects.
2566 /// </summary>
2567 public int GetInstantiatedObjectsId(GameObject go)
2568 {
2569 int id = -1;
2570 if (go == null)
2571 {
2572 Debug.LogError("GetInstantiatedObjectsId() for GO == null.");
2573 return id;
2574 }
2575
2576 PhotonView[] pvs = go.GetPhotonViewsInChildren();
2577 if (pvs != null && pvs.Length > 0 && pvs[0] != null)
2578 {
2579 return pvs[0].instantiationId;
2580 }
2581
2582 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
2583 UnityEngine.Debug.Log("GetInstantiatedObjectsId failed for GO: " + go);
2584
2585
2586 return id;
2587 }
2588
2589 /// <summary>
2590 /// Removes an instantiation event from the server's cache. Needs id and actorNr of player who instantiated.
2591 /// </summary>
2592 private void ServerCleanInstantiateAndDestroy(int instantiateId, int creatorId, bool isRuntimeInstantiated)
2593 {
2594 Hashtable removeFilter = new Hashtable();
2595 removeFilter[(byte)7] = instantiateId;
2596
2597 RaiseEventOptions options = new RaiseEventOptions() { CachingOption = EventCaching.RemoveFromRoomCache, TargetActors = new int[] { creatorId } };
2598 this.OpRaiseEvent(PunEvent.Instantiation, removeFilter, true, options);
2599 //this.OpRaiseEvent(PunEvent.Instantiation, removeFilter, true, 0, new int[] { actorNr }, EventCaching.RemoveFromRoomCache);
2600
2601 Hashtable evData = new Hashtable();
2602 evData[(byte)0] = instantiateId;
2603 options = null;
2604 if (!isRuntimeInstantiated)
2605 {
2606 // if the view got loaded with the scene, the EvDestroy must be cached (there is no Instantiate-msg which we can remove)
2607 // reason: joining players will load the obj and have to destroy it (too)
2608 options = new RaiseEventOptions();
2609 options.CachingOption = EventCaching.AddToRoomCacheGlobal;
2610 Debug.Log("Destroying GO as global. ID: " + instantiateId);
2611 }
2612 this.OpRaiseEvent(PunEvent.Destroy, evData, true, options);
2613 }
2614
2615 private void SendDestroyOfPlayer(int actorNr)
2616 {
2617 Hashtable evData = new Hashtable();
2618 evData[(byte)0] = actorNr;
2619
2620 this.OpRaiseEvent(PunEvent.DestroyPlayer, evData, true, null);
2621 //this.OpRaiseEvent(PunEvent.DestroyPlayer, evData, true, 0, EventCaching.DoNotCache, ReceiverGroup.Others);
2622 }
2623
2624 private void SendDestroyOfAll()
2625 {
2626 Hashtable evData = new Hashtable();
2627 evData[(byte)0] = -1;
2628
2629
2630 this.OpRaiseEvent(PunEvent.DestroyPlayer, evData, true, null);
2631 //this.OpRaiseEvent(PunEvent.DestroyPlayer, evData, true, 0, EventCaching.DoNotCache, ReceiverGroup.Others);
2632 }
2633
2634 private void OpRemoveFromServerInstantiationsOfPlayer(int actorNr)
2635 {
2636 // removes all "Instantiation" events of player actorNr. this is not an event for anyone else
2637 RaiseEventOptions options = new RaiseEventOptions() { CachingOption = EventCaching.RemoveFromRoomCache, TargetActors = new int[] { actorNr } };
2638 this.OpRaiseEvent(PunEvent.Instantiation, null, true, options);
2639 //this.OpRaiseEvent(PunEvent.Instantiation, null, true, 0, new int[] { actorNr }, EventCaching.RemoveFromRoomCache);
2640 }
2641
2642 internal protected void RequestOwnership(int viewID, int fromOwner)
2643 {
2644 Debug.Log("RequestOwnership(): " + viewID + " from: " + fromOwner + " Time: " + Environment.TickCount % 1000);
2645 //PhotonNetwork.networkingPeer.OpRaiseEvent(PunEvent.OwnershipRequest, true, new int[] { viewID, fromOwner }, 0, EventCaching.DoNotCache, null, ReceiverGroup.All, 0);
2646 this.OpRaiseEvent(PunEvent.OwnershipRequest, new int[] {viewID, fromOwner}, true, new RaiseEventOptions() { Receivers = ReceiverGroup.All }); // All sends to all via server (including self)
2647 }
2648
2649 internal protected void TransferOwnership(int viewID, int playerID)
2650 {
2651 Debug.Log("TransferOwnership() view " + viewID + " to: " + playerID + " Time: " + Environment.TickCount % 1000);
2652 //PhotonNetwork.networkingPeer.OpRaiseEvent(PunEvent.OwnershipTransfer, true, new int[] {viewID, playerID}, 0, EventCaching.DoNotCache, null, ReceiverGroup.All, 0);
2653 this.OpRaiseEvent(PunEvent.OwnershipTransfer, new int[] { viewID, playerID }, true, new RaiseEventOptions() { Receivers = ReceiverGroup.All }); // All sends to all via server (including self)
2654 }
2655
2656 public void LocalCleanPhotonView(PhotonView view)
2657 {
2658 view.destroyedByPhotonNetworkOrQuit = true;
2659 this.photonViewList.Remove(view.viewID);
2660 }
2661
2662
2663 public PhotonView GetPhotonView(int viewID)
2664 {
2665 PhotonView result = null;
2666 this.photonViewList.TryGetValue(viewID, out result);
2667
2668 if (result == null)
2669 {
2670 PhotonView[] views = GameObject.FindObjectsOfType(typeof(PhotonView)) as PhotonView[];
2671
2672 foreach (PhotonView view in views)
2673 {
2674 if (view.viewID == viewID)
2675 {
2676 if (view.didAwake)
2677 {
2678 Debug.LogWarning("Had to lookup view that wasn't in photonViewList: " + view);
2679 }
2680 return view;
2681 }
2682 }
2683 }
2684
2685 return result;
2686 }
2687
2688 public void RegisterPhotonView(PhotonView netView)
2689 {
2690 if (!Application.isPlaying)
2691 {
2692 this.photonViewList = new Dictionary<int, PhotonView>();
2693 return;
2694 }
2695
2696 if (netView.viewID == 0)
2697 {
2698 // don't register views with ID 0 (not initialized). they register when a ID is assigned later on
2699 Debug.Log("PhotonView register is ignored, because viewID is 0. No id assigned yet to: " + netView);
2700 return;
2701 }
2702
2703 if (this.photonViewList.ContainsKey(netView.viewID))
2704 {
2705 // if some other view is in the list already, we got a problem. it might be undestructible. print out error
2706 if (netView != photonViewList[netView.viewID])
2707 {
2708 Debug.LogError(string.Format("PhotonView ID duplicate found: {0}. New: {1} old: {2}. Maybe one wasn't destroyed on scene load?! Check for 'DontDestroyOnLoad'. Destroying old entry, adding new.", netView.viewID, netView, photonViewList[netView.viewID]));
2709 }
2710
2711 //this.photonViewList.Remove(netView.viewID); // TODO check if we chould Destroy the GO of this view?!
2712 this.RemoveInstantiatedGO(photonViewList[netView.viewID].gameObject, true);
2713 }
2714
2715 // Debug.Log("adding view to known list: " + netView);
2716 this.photonViewList.Add(netView.viewID, netView);
2717 //Debug.LogError("view being added. " + netView); // Exit Games internal log
2718
2719 if (PhotonNetwork.logLevel >= PhotonLogLevel.Full)
2720 Debug.Log("Registered PhotonView: " + netView.viewID);
2721 }
2722
2723 ///// <summary>
2724 ///// Will remove the view from list of views (by its ID).
2725 ///// </summary>
2726 //public void RemovePhotonView(PhotonView netView)
2727 //{
2728 // if (!Application.isPlaying)
2729 // {
2730 // this.photonViewList = new Dictionary<int, PhotonView>();
2731 // return;
2732 // }
2733
2734 // //PhotonView removedView = null;
2735 // //this.photonViewList.TryGetValue(netView.viewID, out removedView);
2736 // //if (removedView != netView)
2737 // //{
2738 // // Debug.LogError("Detected two differing PhotonViews with same viewID: " + netView.viewID);
2739 // //}
2740
2741 // this.photonViewList.Remove(netView.viewID);
2742
2743 // //if (this.DebugOut >= DebugLevel.ALL)
2744 // //{
2745 // // this.DebugReturn(DebugLevel.ALL, "Removed PhotonView: " + netView.viewID);
2746 // //}
2747 //}
2748
2749 /// <summary>
2750 /// Removes the RPCs of someone else (to be used as master).
2751 /// This won't clean any local caches. It just tells the server to forget a player's RPCs and instantiates.
2752 /// </summary>
2753 /// <param name="actorNumber"></param>
2754 public void OpCleanRpcBuffer(int actorNumber)
2755 {
2756 RaiseEventOptions options = new RaiseEventOptions() { CachingOption = EventCaching.RemoveFromRoomCache, TargetActors = new int[] { actorNumber } };
2757 this.OpRaiseEvent(PunEvent.RPC, null, true, options);
2758 //this.OpRaiseEvent(PunEvent.RPC, null, true, 0, new int[] { actorNumber }, EventCaching.RemoveFromRoomCache);
2759 }
2760
2761 /// <summary>
2762 /// Instead removing RPCs or Instantiates, this removed everything cached by the actor.
2763 /// </summary>
2764 /// <param name="actorNumber"></param>
2765 public void OpRemoveCompleteCacheOfPlayer(int actorNumber)
2766 {
2767 RaiseEventOptions options = new RaiseEventOptions() { CachingOption = EventCaching.RemoveFromRoomCache, TargetActors = new int[] { actorNumber } };
2768 this.OpRaiseEvent(0, null, true, options);
2769 //this.OpRaiseEvent(0, null, true, 0, new int[] { actorNumber }, EventCaching.RemoveFromRoomCache);
2770 }
2771
2772
2773 public void OpRemoveCompleteCache()
2774 {
2775 RaiseEventOptions options = new RaiseEventOptions() { CachingOption = EventCaching.RemoveFromRoomCache, Receivers = ReceiverGroup.MasterClient };
2776 this.OpRaiseEvent(0, null, true, options);
2777 //this.OpRaiseEvent(0, null, true, 0, EventCaching.RemoveFromRoomCache, ReceiverGroup.MasterClient); // TODO: check who gets this event?
2778 }
2779
2780 /// This clears the cache of any player/actor who's no longer in the room (making it a simple clean-up option for a new master)
2781 private void RemoveCacheOfLeftPlayers()
2782 {
2783 Dictionary<byte, object> opParameters = new Dictionary<byte, object>();
2784 opParameters[ParameterCode.Code] = (byte)0; // any event
2785 opParameters[ParameterCode.Cache] = (byte)EventCaching.RemoveFromRoomCacheForActorsLeft; // option to clear the room cache of all events of players who left
2786
2787 this.OpCustom((byte)OperationCode.RaiseEvent, opParameters, true, 0);
2788 }
2789
2790 // Remove RPCs of view (if they are local player's RPCs)
2791 public void CleanRpcBufferIfMine(PhotonView view)
2792 {
2793 if (view.ownerId != this.mLocalActor.ID && !mLocalActor.isMasterClient)
2794 {
2795 Debug.LogError("Cannot remove cached RPCs on a PhotonView thats not ours! " + view.owner + " scene: " + view.isSceneView);
2796 return;
2797 }
2798
2799 this.OpCleanRpcBuffer(view);
2800 }
2801
2802 /// <summary>Cleans server RPCs for PhotonView (without any further checks).</summary>
2803 public void OpCleanRpcBuffer(PhotonView view)
2804 {
2805 Hashtable rpcFilterByViewId = new Hashtable();
2806 rpcFilterByViewId[(byte)0] = view.viewID;
2807
2808 RaiseEventOptions options = new RaiseEventOptions() { CachingOption = EventCaching.RemoveFromRoomCache };
2809 this.OpRaiseEvent(PunEvent.RPC, rpcFilterByViewId, true, options);
2810 //this.OpRaiseEvent(PunEvent.RPC, rpcFilterByViewId, true, 0, EventCaching.RemoveFromRoomCache, ReceiverGroup.Others);
2811 }
2812
2813 public void RemoveRPCsInGroup(int group)
2814 {
2815 foreach (KeyValuePair<int, PhotonView> kvp in this.photonViewList)
2816 {
2817 PhotonView view = kvp.Value;
2818 if (view.group == group)
2819 {
2820 this.CleanRpcBufferIfMine(view);
2821 }
2822 }
2823 }
2824
2825 public void SetLevelPrefix(short prefix)
2826 {
2827 this.currentLevelPrefix = prefix;
2828 // TODO: should we really change the prefix for existing PVs?! better keep it!
2829 //foreach (PhotonView view in this.photonViewList.Values)
2830 //{
2831 // view.prefix = prefix;
2832 //}
2833 }
2834
2835 internal void RPC(PhotonView view, string methodName, PhotonPlayer player, bool encrypt, params object[] parameters)
2836 {
2837 if (this.blockSendingGroups.Contains(view.group))
2838 {
2839 return; // Block sending on this group
2840 }
2841
2842 if (view.viewID < 1) //TODO: check why 0 should be illegal
2843 {
2844 Debug.LogError("Illegal view ID:" + view.viewID + " method: " + methodName + " GO:" + view.gameObject.name);
2845 }
2846
2847 if (PhotonNetwork.logLevel >= PhotonLogLevel.Full)
2848 {
2849 Debug.Log("Sending RPC \"" + methodName + "\" to player[" + player + "]");
2850 }
2851
2852
2853 //ts: changed RPCs to a one-level hashtable as described in internal.txt
2854 Hashtable rpcEvent = new Hashtable();
2855 rpcEvent[(byte)0] = (int)view.viewID; // LIMITS PHOTONVIEWS&PLAYERS
2856 if (view.prefix > 0)
2857 {
2858 rpcEvent[(byte)1] = (short)view.prefix;
2859 }
2860 rpcEvent[(byte)2] = this.ServerTimeInMilliSeconds;
2861
2862 // send name or shortcut (if available)
2863 int shortcut = 0;
2864 if (rpcShortcuts.TryGetValue(methodName, out shortcut))
2865 {
2866 rpcEvent[(byte)5] = (byte)shortcut; // LIMITS RPC COUNT
2867 }
2868 else
2869 {
2870 rpcEvent[(byte)3] = methodName;
2871 }
2872
2873 if (parameters != null && parameters.Length > 0)
2874 {
2875 rpcEvent[(byte) 4] = (object[]) parameters;
2876 }
2877
2878 if (this.mLocalActor == player)
2879 {
2880 this.ExecuteRPC(rpcEvent, player);
2881 }
2882 else
2883 {
2884 RaiseEventOptions options = new RaiseEventOptions() { TargetActors = new int[] { player.ID }, Encrypt = encrypt };
2885 this.OpRaiseEvent(PunEvent.RPC, rpcEvent, true, options);
2886 }
2887 }
2888
2889 /// RPC Hashtable Structure
2890 /// (byte)0 -> (int) ViewId (combined from actorNr and actor-unique-id)
2891 /// (byte)1 -> (short) prefix (level)
2892 /// (byte)2 -> (int) server timestamp
2893 /// (byte)3 -> (string) methodname
2894 /// (byte)4 -> (object[]) parameters
2895 /// (byte)5 -> (byte) method shortcut (alternative to name)
2896 ///
2897 /// This is sent as event (code: 200) which will contain a sender (origin of this RPC).
2898
2899 internal void RPC(PhotonView view, string methodName, PhotonTargets target, bool encrypt, params object[] parameters)
2900 {
2901 if (this.blockSendingGroups.Contains(view.group))
2902 {
2903 return; // Block sending on this group
2904 }
2905
2906 if (view.viewID < 1)
2907 {
2908 Debug.LogError("Illegal view ID:" + view.viewID + " method: " + methodName + " GO:" + view.gameObject.name);
2909 }
2910
2911 if (PhotonNetwork.logLevel >= PhotonLogLevel.Full)
2912 Debug.Log("Sending RPC \"" + methodName + "\" to " + target);
2913
2914
2915 //ts: changed RPCs to a one-level hashtable as described in internal.txt
2916 Hashtable rpcEvent = new Hashtable();
2917 rpcEvent[(byte)0] = (int)view.viewID; // LIMITS NETWORKVIEWS&PLAYERS
2918 if (view.prefix > 0)
2919 {
2920 rpcEvent[(byte)1] = (short)view.prefix;
2921 }
2922 rpcEvent[(byte)2] = this.ServerTimeInMilliSeconds;
2923
2924
2925 // send name or shortcut (if available)
2926 int shortcut = 0;
2927 if (rpcShortcuts.TryGetValue(methodName, out shortcut))
2928 {
2929 rpcEvent[(byte)5] = (byte)shortcut; // LIMITS RPC COUNT
2930 }
2931 else
2932 {
2933 rpcEvent[(byte)3] = methodName;
2934 }
2935
2936 if (parameters != null && parameters.Length > 0)
2937 {
2938 rpcEvent[(byte)4] = (object[])parameters;
2939 }
2940
2941 // Check scoping
2942 if (target == PhotonTargets.All)
2943 {
2944 RaiseEventOptions options = new RaiseEventOptions() { InterestGroup = (byte)view.group, Encrypt = encrypt };
2945 this.OpRaiseEvent(PunEvent.RPC, rpcEvent, true, options);
2946
2947 // Execute local
2948 this.ExecuteRPC(rpcEvent, this.mLocalActor);
2949 }
2950 else if (target == PhotonTargets.Others)
2951 {
2952 RaiseEventOptions options = new RaiseEventOptions() { InterestGroup = (byte)view.group, Encrypt = encrypt };
2953 this.OpRaiseEvent(PunEvent.RPC, rpcEvent, true, options);
2954 }
2955 else if (target == PhotonTargets.AllBuffered)
2956 {
2957 RaiseEventOptions options = new RaiseEventOptions() { CachingOption = EventCaching.AddToRoomCache, Encrypt = encrypt };
2958 this.OpRaiseEvent(PunEvent.RPC, rpcEvent, true, options);
2959
2960 // Execute local
2961 this.ExecuteRPC(rpcEvent, this.mLocalActor);
2962 }
2963 else if (target == PhotonTargets.OthersBuffered)
2964 {
2965 RaiseEventOptions options = new RaiseEventOptions() { CachingOption = EventCaching.AddToRoomCache, Encrypt = encrypt };
2966 this.OpRaiseEvent(PunEvent.RPC, rpcEvent, true, options);
2967 }
2968 else if (target == PhotonTargets.MasterClient)
2969 {
2970 if (this.mMasterClient == this.mLocalActor)
2971 {
2972 this.ExecuteRPC(rpcEvent, this.mLocalActor);
2973 }
2974 else
2975 {
2976 RaiseEventOptions options = new RaiseEventOptions() { Receivers = ReceiverGroup.MasterClient, Encrypt = encrypt };
2977 this.OpRaiseEvent(PunEvent.RPC, rpcEvent, true, options);
2978 }
2979 }
2980 else if (target == PhotonTargets.AllViaServer)
2981 {
2982 RaiseEventOptions options = new RaiseEventOptions() { InterestGroup = (byte)view.group, Receivers = ReceiverGroup.All, Encrypt = encrypt };
2983 this.OpRaiseEvent(PunEvent.RPC, rpcEvent, true, options);
2984 if (PhotonNetwork.offlineMode)
2985 {
2986 this.ExecuteRPC(rpcEvent, this.mLocalActor);
2987 }
2988 }
2989 else if (target == PhotonTargets.AllBufferedViaServer)
2990 {
2991 RaiseEventOptions options = new RaiseEventOptions() { InterestGroup = (byte)view.group, Receivers = ReceiverGroup.All, CachingOption = EventCaching.AddToRoomCache, Encrypt = encrypt };
2992 this.OpRaiseEvent(PunEvent.RPC, rpcEvent, true, options);
2993 if (PhotonNetwork.offlineMode)
2994 {
2995 this.ExecuteRPC(rpcEvent, this.mLocalActor);
2996 }
2997 }
2998 else
2999 {
3000 Debug.LogError("Unsupported target enum: " + target);
3001 }
3002 }
3003
3004 // SetReceiving
3005 public void SetReceivingEnabled(int group, bool enabled)
3006 {
3007 if (group <= 0)
3008 {
3009 Debug.LogError("Error: PhotonNetwork.SetReceivingEnabled was called with an illegal group number: " + group + ". The group number should be at least 1.");
3010 return;
3011 }
3012
3013 if (enabled)
3014 {
3015 if (!this.allowedReceivingGroups.Contains(group))
3016 {
3017 this.allowedReceivingGroups.Add(group);
3018 byte[] groups = new byte[1] { (byte)group };
3019 this.OpChangeGroups(null, groups);
3020 }
3021 }
3022 else
3023 {
3024 if (this.allowedReceivingGroups.Contains(group))
3025 {
3026 this.allowedReceivingGroups.Remove(group);
3027 byte[] groups = new byte[1] { (byte)group };
3028 this.OpChangeGroups(groups, null);
3029 }
3030 }
3031 }
3032
3033
3034 public void SetReceivingEnabled(int[] enableGroups, int[] disableGroups)
3035 {
3036 List<byte> enableList = new List<byte>();
3037 List<byte> disableList = new List<byte>();
3038
3039 if (enableGroups != null)
3040 {
3041 for (int index = 0; index < enableGroups.Length; index++)
3042 {
3043 int i = enableGroups[index];
3044 if (i <= 0)
3045 {
3046 Debug.LogError("Error: PhotonNetwork.SetReceivingEnabled was called with an illegal group number: " + i + ". The group number should be at least 1.");
3047 continue;
3048 }
3049 if (!this.allowedReceivingGroups.Contains(i))
3050 {
3051 this.allowedReceivingGroups.Add(i);
3052 enableList.Add((byte)i);
3053 }
3054 }
3055 }
3056 if (disableGroups != null)
3057 {
3058 for (int index = 0; index < disableGroups.Length; index++)
3059 {
3060 int i = disableGroups[index];
3061 if (i <= 0)
3062 {
3063 Debug.LogError("Error: PhotonNetwork.SetReceivingEnabled was called with an illegal group number: " + i + ". The group number should be at least 1.");
3064 continue;
3065 }
3066 if (enableList.Contains((byte)i))
3067 {
3068 Debug.LogError("Error: PhotonNetwork.SetReceivingEnabled disableGroups contains a group that is also in the enableGroups: " + i + ".");
3069 continue;
3070 }
3071 if (this.allowedReceivingGroups.Contains(i))
3072 {
3073 this.allowedReceivingGroups.Remove(i);
3074 disableList.Add((byte)i);
3075 }
3076 }
3077 }
3078
3079 this.OpChangeGroups(disableList.Count > 0 ? disableList.ToArray() : null, enableList.Count > 0 ? enableList.ToArray() : null); //Passing a 0 sized array != passing null
3080 }
3081
3082 // SetSending
3083 public void SetSendingEnabled(int group, bool enabled)
3084 {
3085 if (!enabled)
3086 {
3087 this.blockSendingGroups.Add(group); // can be added to HashSet no matter if already in it
3088 }
3089 else
3090 {
3091 this.blockSendingGroups.Remove(group);
3092 }
3093 }
3094
3095
3096 public void SetSendingEnabled(int[] enableGroups, int[] disableGroups)
3097 {
3098 if(enableGroups!=null){
3099 foreach(int i in enableGroups){
3100 if(this.blockSendingGroups.Contains(i))
3101 this.blockSendingGroups.Remove(i);
3102 }
3103 }
3104 if(disableGroups!=null){
3105 foreach(int i in disableGroups){
3106 if(!this.blockSendingGroups.Contains(i))
3107 this.blockSendingGroups.Add(i);
3108 }
3109 }
3110 }
3111
3112
3113 public void NewSceneLoaded()
3114 {
3115 if (this.loadingLevelAndPausedNetwork)
3116 {
3117 this.loadingLevelAndPausedNetwork = false;
3118 PhotonNetwork.isMessageQueueRunning = true;
3119 }
3120 // Debug.Log("OnLevelWasLoaded photonViewList.Count: " + photonViewList.Count); // Exit Games internal log
3121
3122 List<int> removeKeys = new List<int>();
3123 foreach (KeyValuePair<int, PhotonView> kvp in this.photonViewList)
3124 {
3125 PhotonView view = kvp.Value;
3126 if (view == null)
3127 {
3128 removeKeys.Add(kvp.Key);
3129 }
3130 }
3131
3132 for (int index = 0; index < removeKeys.Count; index++)
3133 {
3134 int key = removeKeys[index];
3135 this.photonViewList.Remove(key);
3136 }
3137
3138 if (removeKeys.Count > 0)
3139 {
3140 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
3141 Debug.Log("New level loaded. Removed " + removeKeys.Count + " scene view IDs from last level.");
3142 }
3143 }
3144
3145
3146 // this is called by Update() and in Unity that means it's single threaded.
3147 public void RunViewUpdate()
3148 {
3149 if (!PhotonNetwork.connected || PhotonNetwork.offlineMode)
3150 {
3151 return;
3152 }
3153
3154 if (this.mActors == null ||
3155 #if !PHOTON_DEVELOP
3156 this.mActors.Count <= 1
3157 #endif
3158 )
3159 {
3160 return; // No need to send OnSerialize messages (these are never buffered anyway)
3161 }
3162
3163 dataPerGroupReliable.Clear();
3164 dataPerGroupUnreliable.Clear();
3165
3166 /* Format of the data hashtable:
3167 * Hasthable dataPergroup*
3168 * [(byte)0] = this.ServerTimeInMilliSeconds;
3169 * OPTIONAL: [(byte)1] = currentLevelPrefix;
3170 * + data
3171 */
3172
3173 foreach (KeyValuePair<int, PhotonView> kvp in this.photonViewList)
3174 {
3175 PhotonView view = kvp.Value;
3176
3177 if (view.synchronization != ViewSynchronization.Off)
3178 {
3179 // Fetch all sending photonViews
3180 if (view.isMine)
3181 {
3182 #if UNITY_2_6_1 || UNITY_2_6 || UNITY_3_0 || UNITY_3_0_0 || UNITY_3_1 || UNITY_3_2 || UNITY_3_3 || UNITY_3_4 || UNITY_3_5
3183 if (!view.gameObject.active)
3184 {
3185 continue; // Only on actives
3186 }
3187 #else
3188 if (!view.gameObject.activeInHierarchy)
3189 {
3190 continue; // Only on actives
3191 }
3192 #endif
3193
3194 if (this.blockSendingGroups.Contains(view.group))
3195 {
3196 continue; // Block sending on this group
3197 }
3198
3199 // Run it trough its OnSerialize
3200 Hashtable evData = this.OnSerializeWrite(view);
3201 if (evData == null)
3202 {
3203 continue;
3204 }
3205
3206 if (view.synchronization == ViewSynchronization.ReliableDeltaCompressed || view.mixedModeIsReliable)
3207 {
3208 if (!evData.ContainsKey((byte)1) && !evData.ContainsKey((byte)2))
3209 {
3210 // Everything has been removed by compression, nothing to send
3211 }
3212 else
3213 {
3214 if (!dataPerGroupReliable.ContainsKey(view.group))
3215 {
3216 dataPerGroupReliable[view.group] = new Hashtable();
3217 dataPerGroupReliable[view.group][(byte)0] = this.ServerTimeInMilliSeconds;
3218 if (currentLevelPrefix >= 0)
3219 {
3220 dataPerGroupReliable[view.group][(byte)1] = this.currentLevelPrefix;
3221 }
3222 }
3223 Hashtable groupHashtable = dataPerGroupReliable[view.group];
3224 groupHashtable.Add((short)groupHashtable.Count, evData);
3225 }
3226 }
3227 else
3228 {
3229 if (!dataPerGroupUnreliable.ContainsKey(view.group))
3230 {
3231 dataPerGroupUnreliable[view.group] = new Hashtable();
3232 dataPerGroupUnreliable[view.group][(byte)0] = this.ServerTimeInMilliSeconds;
3233 if (currentLevelPrefix >= 0)
3234 {
3235 dataPerGroupUnreliable[view.group][(byte)1] = this.currentLevelPrefix;
3236 }
3237 }
3238 Hashtable groupHashtable = dataPerGroupUnreliable[view.group];
3239 groupHashtable.Add((short)groupHashtable.Count, evData);
3240 }
3241 }
3242 else
3243 {
3244 // Debug.Log(" NO OBS on " + view.name + " " + view.owner);
3245 }
3246 }
3247 else
3248 {
3249 }
3250 }
3251
3252 //Send the messages: every group is send in it's own message and unreliable and reliable are split as well
3253 RaiseEventOptions options = new RaiseEventOptions();
3254
3255 #if PHOTON_DEVELOP
3256 options.Receivers = ReceiverGroup.All;
3257 #endif
3258
3259 foreach (KeyValuePair<int, Hashtable> kvp in dataPerGroupReliable)
3260 {
3261 options.InterestGroup = (byte)kvp.Key;
3262 this.OpRaiseEvent(PunEvent.SendSerializeReliable, kvp.Value, true, options);
3263 }
3264 foreach (KeyValuePair<int, Hashtable> kvp in dataPerGroupUnreliable)
3265 {
3266 options.InterestGroup = (byte)kvp.Key;
3267 this.OpRaiseEvent(PunEvent.SendSerialize, kvp.Value, false, options);
3268 }
3269 }
3270
3271 // calls OnPhotonSerializeView (through ExecuteOnSerialize)
3272 // the content created here is consumed by receivers in: ReadOnSerialize
3273 private Hashtable OnSerializeWrite(PhotonView view)
3274 {
3275 PhotonStream pStream = new PhotonStream( true, null );
3276 PhotonMessageInfo info = new PhotonMessageInfo( this.mLocalActor, this.ServerTimeInMilliSeconds, view );
3277
3278 // each view creates a list of values that should be sent
3279 view.SerializeView( pStream, info );
3280
3281 if( pStream.Count == 0 )
3282 {
3283 return null;
3284 }
3285
3286 object[] dataArray = pStream.data.ToArray();
3287
3288 if (view.synchronization == ViewSynchronization.UnreliableOnChange)
3289 {
3290 if (AlmostEquals(dataArray, view.lastOnSerializeDataSent))
3291 {
3292 if (view.mixedModeIsReliable)
3293 {
3294 return null;
3295 }
3296
3297 view.mixedModeIsReliable = true;
3298 view.lastOnSerializeDataSent = dataArray;
3299 }
3300 else
3301 {
3302 view.mixedModeIsReliable = false;
3303 view.lastOnSerializeDataSent = dataArray;
3304 }
3305 }
3306
3307 // EVDATA:
3308 // 0=View ID (an int, never compressed cause it's not in the data)
3309 // 1=data of observed type (different per type of observed object)
3310 // 2=compressed data (in this case, key 1 is empty)
3311 // 3=list of values that are actually null (if something was changed but actually IS null)
3312 Hashtable evData = new Hashtable();
3313 evData[(byte)0] = (int)view.viewID;
3314 evData[(byte)1] = dataArray; // this is the actual data (script or observed object)
3315
3316
3317 if (view.synchronization == ViewSynchronization.ReliableDeltaCompressed)
3318 {
3319 // compress content of data set (by comparing to view.lastOnSerializeDataSent)
3320 // the "original" dataArray is NOT modified by DeltaCompressionWrite
3321 // if something was compressed, the evData key 2 and 3 are used (see above)
3322 bool somethingLeftToSend = this.DeltaCompressionWrite(view, evData);
3323
3324 // buffer the full data set (for next compression)
3325 view.lastOnSerializeDataSent = dataArray;
3326
3327 if (!somethingLeftToSend)
3328 {
3329 return null;
3330 }
3331 }
3332
3333 return evData;
3334 }
3335
3336 /// <summary>
3337 /// Reads updates created by OnSerializeWrite
3338 /// </summary>
3339 private void OnSerializeRead(Hashtable data, PhotonPlayer sender, int networkTime, short correctPrefix)
3340 {
3341 // read view ID from key (byte)0: a int-array (PUN 1.17++)
3342 int viewID = (int)data[(byte)0];
3343
3344
3345 PhotonView view = this.GetPhotonView(viewID);
3346 if (view == null)
3347 {
3348 Debug.LogWarning("Received OnSerialization for view ID " + viewID + ". We have no such PhotonView! Ignored this if you're leaving a room. State: " + this.State);
3349 return;
3350 }
3351
3352 if (view.prefix > 0 && correctPrefix != view.prefix)
3353 {
3354 Debug.LogError("Received OnSerialization for view ID " + viewID + " with prefix " + correctPrefix + ". Our prefix is " + view.prefix);
3355 return;
3356 }
3357
3358 // SetReceiving filtering
3359 if (view.group != 0 && !this.allowedReceivingGroups.Contains(view.group))
3360 {
3361 return; // Ignore group
3362 }
3363
3364
3365 if (view.synchronization == ViewSynchronization.ReliableDeltaCompressed)
3366 {
3367 if (!this.DeltaCompressionRead(view, data))
3368 {
3369 // Skip this packet as we haven't got received complete-copy of this view yet.
3370 if (PhotonNetwork.logLevel >= PhotonLogLevel.Informational)
3371 Debug.Log("Skipping packet for " + view.name + " [" + view.viewID + "] as we haven't received a full packet for delta compression yet. This is OK if it happens for the first few frames after joining a game.");
3372 return;
3373 }
3374
3375 // store last received for delta-compression usage
3376 view.lastOnSerializeDataReceived = data[(byte)1] as object[];
3377 }
3378
3379 if (sender.ID != view.ownerId)
3380 {
3381 if (!view.isSceneView || !sender.isMasterClient)
3382 {
3383 // obviously the owner changed and we didn't yet notice.
3384 Debug.Log("Adjusting owner to sender of updates. From: " + view.ownerId + " to: " + sender.ID);
3385 view.ownerId = sender.ID;
3386 }
3387 }
3388
3389 object[] contents = data[(byte)1] as object[];
3390 PhotonStream pStream = new PhotonStream(false, contents);
3391 PhotonMessageInfo info = new PhotonMessageInfo(sender, networkTime, view);
3392
3393 view.DeserializeView( pStream, info );
3394 }
3395
3396 private bool AlmostEquals(object[] lastData, object[] currentContent)
3397 {
3398 if (lastData == null && currentContent == null)
3399 {
3400 return true;
3401 }
3402
3403 if (lastData == null || currentContent == null || (lastData.Length != currentContent.Length))
3404 {
3405 return false;
3406 }
3407
3408 for (int index = 0; index < currentContent.Length; index++)
3409 {
3410 object newObj = currentContent[index];
3411 object oldObj = lastData[index];
3412 if (!this.ObjectIsSameWithInprecision(newObj, oldObj))
3413 {
3414 return false;
3415 }
3416 }
3417
3418 return true;
3419 }
3420
3421 /// <summary>
3422 /// Compares the new data with previously sent data and skips values that didn't change.
3423 /// </summary>
3424 /// <returns>True if anything has to be sent, false if nothing new or no data</returns>
3425 private bool DeltaCompressionWrite(PhotonView view, Hashtable data)
3426 {
3427 if (view.lastOnSerializeDataSent == null)
3428 {
3429 return true; // all has to be sent
3430 }
3431
3432 // We can compress as we sent a full update previously (readers can re-use previous values)
3433 object[] lastData = view.lastOnSerializeDataSent;
3434 object[] currentContent = data[(byte)1] as object[];
3435
3436 if (currentContent == null)
3437 {
3438 // no data to be sent
3439 return false;
3440 }
3441
3442 if (lastData.Length != currentContent.Length)
3443 {
3444 // if new data isn't same length as before, we send the complete data-set uncompressed
3445 return true;
3446 }
3447
3448 object[] compressedContent = new object[currentContent.Length];
3449 int compressedValues = 0;
3450
3451 List<int> valuesThatAreChangedToNull = new List<int>();
3452 for (int index = 0; index < compressedContent.Length; index++)
3453 {
3454 object newObj = currentContent[index];
3455 object oldObj = lastData[index];
3456 if (this.ObjectIsSameWithInprecision(newObj, oldObj))
3457 {
3458 // compress (by using null, instead of value, which is same as before)
3459 compressedValues++;
3460 // compressedContent[index] is already null (initialized)
3461 }
3462 else
3463 {
3464 compressedContent[index] = currentContent[index];
3465
3466 // value changed, we don't replace it with null
3467 // new value is null (like a compressed value): we have to mark it so it STAYS null instead of being replaced with previous value
3468 if (newObj == null)
3469 {
3470 valuesThatAreChangedToNull.Add(index);
3471 }
3472 }
3473 }
3474
3475 // Only send the list of compressed fields if we actually compressed 1 or more fields.
3476 if (compressedValues > 0)
3477 {
3478 data.Remove((byte)1); // remove the original data (we only send compressed data)
3479
3480 if (compressedValues == currentContent.Length)
3481 {
3482 // all values are compressed to null, we have nothing to send
3483 return false;
3484 }
3485
3486 data[(byte)2] = compressedContent; // current, compressted data is moved to key 2 to mark it as compressed
3487 if (valuesThatAreChangedToNull.Count > 0)
3488 {
3489 data[(byte)3] = valuesThatAreChangedToNull.ToArray(); // data that is actually null (not just cause we didn't want to send it)
3490 }
3491 }
3492
3493 return true; // some data was compressed but we need to send something
3494 }
3495
3496 /// <summary>
3497 /// reads incoming messages created by "OnSerialize"
3498 /// </summary>
3499 private bool DeltaCompressionRead(PhotonView view, Hashtable data)
3500 {
3501 if (data.ContainsKey((byte)1))
3502 {
3503 // we have a full list of data (cause key 1 is used), so return "we have uncompressed all"
3504 return true;
3505 }
3506
3507 // Compression was applied as data[(byte)2] exists (this is the data with some fields being compressed to null)
3508 // now we also need a previous "full" list of values to restore values that are null in this msg
3509 if (view.lastOnSerializeDataReceived == null)
3510 {
3511 return false; // We dont have a full match yet, we cannot work with missing values: skip this message
3512 }
3513
3514 object[] compressedContents = data[(byte)2] as object[];
3515 if (compressedContents == null)
3516 {
3517 // despite expectation, there is no compressed data in this msg. shouldn't happen. just a null check
3518 return false;
3519 }
3520
3521 int[] indexesThatAreChangedToNull = data[(byte)3] as int[];
3522 if (indexesThatAreChangedToNull == null)
3523 {
3524 indexesThatAreChangedToNull = new int[0];
3525 }
3526
3527 object[] lastReceivedData = view.lastOnSerializeDataReceived;
3528 for (int index = 0; index < compressedContents.Length; index++)
3529 {
3530 if (compressedContents[index] == null && !indexesThatAreChangedToNull.Contains(index))
3531 {
3532 // we replace null values in this received msg unless a index is in the "changed to null" list
3533 object lastValue = lastReceivedData[index];
3534 compressedContents[index] = lastValue;
3535 }
3536 }
3537
3538 data[(byte)1] = compressedContents; // compressedContents are now uncompressed...
3539 return true;
3540 }
3541
3542 /// <summary>
3543 /// Returns true if both objects are almost identical.
3544 /// Used to check whether two objects are similar enough to skip an update.
3545 /// </summary>
3546 bool ObjectIsSameWithInprecision(object one, object two)
3547 {
3548 if (one == null || two == null)
3549 {
3550 return one == null && two == null;
3551 }
3552
3553 if (!one.Equals(two))
3554 {
3555 // if A is not B, lets check if A is almost B
3556 if (one is Vector3)
3557 {
3558 Vector3 a = (Vector3)one;
3559 Vector3 b = (Vector3)two;
3560 if (a.AlmostEquals(b, PhotonNetwork.precisionForVectorSynchronization))
3561 {
3562 return true;
3563 }
3564 }
3565 else if (one is Vector2)
3566 {
3567 Vector2 a = (Vector2)one;
3568 Vector2 b = (Vector2)two;
3569 if (a.AlmostEquals(b, PhotonNetwork.precisionForVectorSynchronization))
3570 {
3571 return true;
3572 }
3573 }
3574 else if (one is Quaternion)
3575 {
3576 Quaternion a = (Quaternion)one;
3577 Quaternion b = (Quaternion)two;
3578 if (a.AlmostEquals(b, PhotonNetwork.precisionForQuaternionSynchronization))
3579 {
3580 return true;
3581 }
3582 }
3583 else if (one is float)
3584 {
3585 float a = (float)one;
3586 float b = (float)two;
3587 if (a.AlmostEquals(b, PhotonNetwork.precisionForFloatSynchronization))
3588 {
3589 return true;
3590 }
3591 }
3592
3593 // one does not equal two
3594 return false;
3595 }
3596
3597 return true;
3598 }
3599
3600 internal protected static bool GetMethod(MonoBehaviour monob, string methodType, out MethodInfo mi)
3601 {
3602 mi = null;
3603
3604 if (monob == null || string.IsNullOrEmpty(methodType))
3605 {
3606 return false;
3607 }
3608
3609 List<MethodInfo> methods = SupportClass.GetMethods(monob.GetType(), null);
3610 for (int index = 0; index < methods.Count; index++)
3611 {
3612 MethodInfo methodInfo = methods[index];
3613 if (methodInfo.Name.Equals(methodType))
3614 {
3615 mi = methodInfo;
3616 return true;
3617 }
3618 }
3619
3620 return false;
3621 }
3622
3623 /// <summary>Internally used to flag if the message queue was disabled by a "scene sync" situation (to re-enable it).</summary>
3624 internal protected bool loadingLevelAndPausedNetwork = false;
3625
3626 /// <summary>For automatic scene syncing, the loaded scene is put into a room property. This is the name of said prop.</summary>
3627 internal protected const string CurrentSceneProperty = "curScn";
3628
3629 /// <summary>Internally used to detect the current scene and load it if PhotonNetwork.automaticallySyncScene is enabled.</summary>
3630 internal protected void LoadLevelIfSynced()
3631 {
3632 if (!PhotonNetwork.automaticallySyncScene || PhotonNetwork.isMasterClient || PhotonNetwork.room == null)
3633 {
3634 return;
3635 }
3636
3637 // check if "current level" is set in props
3638 if (!PhotonNetwork.room.customProperties.ContainsKey(NetworkingPeer.CurrentSceneProperty))
3639 {
3640 return;
3641 }
3642
3643 // if loaded level is not the one defined my master in props, load that level
3644 object sceneId = PhotonNetwork.room.customProperties[NetworkingPeer.CurrentSceneProperty];
3645 if (sceneId is int)
3646 {
3647 if (Application.loadedLevel != (int)sceneId)
3648 PhotonNetwork.LoadLevel((int)sceneId);
3649 }
3650 else if (sceneId is string)
3651 {
3652 if (Application.loadedLevelName != (string)sceneId)
3653 PhotonNetwork.LoadLevel((string)sceneId);
3654 }
3655 }
3656
3657 protected internal void SetLevelInPropsIfSynced(object levelId)
3658 {
3659 if (!PhotonNetwork.automaticallySyncScene || !PhotonNetwork.isMasterClient || PhotonNetwork.room == null)
3660 {
3661 return;
3662 }
3663 if (levelId == null)
3664 {
3665 Debug.LogError("Parameter levelId can't be null!");
3666 return;
3667 }
3668
3669 // check if "current level" is already set in props
3670 if (PhotonNetwork.room.customProperties.ContainsKey(NetworkingPeer.CurrentSceneProperty))
3671 {
3672 object levelIdInProps = PhotonNetwork.room.customProperties[NetworkingPeer.CurrentSceneProperty];
3673 if (levelIdInProps is int && Application.loadedLevel == (int)levelIdInProps)
3674 {
3675 return;
3676 }
3677 if (levelIdInProps is string && Application.loadedLevelName.Equals((string)levelIdInProps))
3678 {
3679 return;
3680 }
3681 }
3682
3683 // current level is not yet in props, so this client has to set it
3684 Hashtable setScene = new Hashtable();
3685 if (levelId is int) setScene[NetworkingPeer.CurrentSceneProperty] = (int)levelId;
3686 else if (levelId is string) setScene[NetworkingPeer.CurrentSceneProperty] = (string)levelId;
3687 else Debug.LogError("Parameter levelId must be int or string!");
3688
3689 PhotonNetwork.room.SetCustomProperties(setScene);
3690 this.SendOutgoingCommands(); // send immediately! because: in most cases the client will begin to load and not send for a while
3691 }
3692
3693 public void SetApp(string appId, string gameVersion)
3694 {
3695 this.mAppId = appId.Trim();
3696
3697 if (!string.IsNullOrEmpty(gameVersion))
3698 {
3699 this.mAppVersion = gameVersion.Trim();
3700 }
3701 }
3702
3703
3704 public bool WebRpc(string uriPath, object parameters)
3705 {
3706 Dictionary<byte, object> opParameters = new Dictionary<byte, object>();
3707 opParameters.Add(ParameterCode.UriPath, uriPath);
3708 opParameters.Add(ParameterCode.WebRpcParameters, parameters);
3709
3710 return this.OpCustom(OperationCode.WebRpc, opParameters, true);
3711
3712 }
3713 }
--------------------------------------------------------------------------------------------------------------------
Part of: Photon Unity Networking (PUN)
--------------------------------------------------------------------------------------------------------------------
Implements Photon LoadBalancing used in PUN.
This class is used internally by PhotonNetwork and not intended as public API.
A user's authentication values used during connect for Custom Authentication with Photon (and a custom servicecommunity).
Set these before calling Connect if you want custom authentication.
Only when in a room
"public" access to the current game - is null unless a room is joined on a gameserver
keeps the custom properties, gameServer address and anything else about the room we want to get into
internal protected Dictionary
private readonly Dictionary
private readonly Dictionary
private readonly Dictionary
public string NameServerAddressHttp = "http:ns.exitgamescloud.com:80photonn";
private static readonly Dictionary
TODO: comment in the code again, when the new auth-workflow is available in the Cloud
return false; this.CustomAuthenticationValues != null && !String.IsNullOrEmpty(this.CustomAuthenticationValues.Secret);
this automatically uses a separate assembly-file with Win8-style Socket usage (not possible in Editor)
PhotonHandler.PingImplementation = typeof(PingWindowsStore); but for ping, we have to set the implementation explicitly to Win 8 StorePhone
#pragma warning disable 0162 the library variant defines if we should use PUN's SocketUdp variant (at all)
this automatically uses a separate assembly-file with Win8-style Socket usage (not possible in Editor)
don't set the field directly! the listener is passed on to other classes, which get updated by the property set method
RPC shortcut lookup creation (from list of RPCs, which is updated by Editor scripts)
connect might fail, if the DNS name can't be resolved or if no network connection is available
Connects to the NameServer for Photon Cloud, where a region and server list can be obtained.
Connects you to a specific region's Master Server, using the Name Server to find the IP.
Debug.Log("Server to connect to: "+ address + " settings protocol: " + PhotonNetwork.PhotonServerSettings.Protocol);
While on the NameServer, this gets you the list of regional servers (short names and their IPs to ping them).
Complete disconnect from photon (and the open master OR game server)
this.LeftRoomCleanup();
this.LeftLobbyCleanup();
Internally used only. Triggers OnStateChange with "Disconnect" in next dispatch which is the signal to re-connect (if at all).
LeftLobbyCleanup();
this.LeftRoomCleanup();
Called at disconnectleavelobby etc. This CAN also be called when we are not in a lobby (e.g. disconnect from room)
Called when "this client" left a room to clean up.
when leaving a room, we clean up depending on that room's settings.
Cleanup all network objects (all spawned PhotonViews, local and remote)
PhotonNetwork.manuallyAllocatedViewIds = new List
Cleans up anything that was instantiated in-game (not loaded with the scene).
Destroy GO's (if we should)
Fill list with Instantiated objects
instantiatedGos.Add(view.gameObject); HashSet keeps each object only once
photonViewList is cleared of anything instantiated (so scene items are left inside)
any other lists can be
this.tempInstantiationData.Clear(); should be empty but to be safe we clear (no new list needed)
gameID can be null (optional). The server assigns a unique name if no name is set
joins a room and sets your current username as custom actorproperty (will broadcast that)
Debug.LogWarning("ReadoutProperties gameProperties: " + gameProperties.ToStringFull() + " pActorProperties: " + pActorProperties.ToStringFull() + " targetActorNr: " + targetActorNr);
read game properties and cache them locally
this.LoadLevelIfSynced(); will load new scene if sceneName was changed
we have a single entry in the pActorProperties with one
user's name
targets MUST exist before you set properties
in this case, we've got a key-value pair per actor (each
value is a hashtable with the actor's properties then)
Resets the PhotonView "lastOnSerializeDataSent" so that "OnReliable" synched PhotonViews send a complete state to new clients (if the state doesnt change, no messages would be send otherwise!).
Note that due to this reset, ALL other players will receive the full OnSerialize.
Called when the event Leave (of some other player) arrived.
Cleans game objects, views locally. The master will also clean the
ID of player who left.
actorNr is fetched out of event above
having a new master before calling destroy for the leaving player is important!
so we elect a new masterclient and ignore the leaving player (who is still in playerlists).
destroy objects & buffered messages
finally, send notification (the playerList and masterclient are now updated)
The ignored player is the one who's leaving and should not become master (again). Pass -1 to select any player from the list.
return early if SOME player (leavingId > 0) is leaving AND it's NOT the current master
picking the player with lowest ID (longest in game).
keys in mActors are their actorNumbers
make a callback ONLY when a playerMaster left
Returns the lowest player.ID - used for Master Client picking.
SendMonoMessage(PhotonNetworkingMessage.OnMasterClientSwitched, this.mMasterClient); we only callback when an actual change is done
this means, the join on the gameServer is sent (with an outdated name). send the new when in game
the local player's actor-properties are not returned in join-result. add this player to the list
the mono message for this is sent at another place
this.mRoomToEnterLobby = typedLobby ?? ((this.insideLobby) ? this.lobby : null); use given lobby, or active lobby (if any active) or none
roomOptions and typedLobby will be null, unless createIfNotExists is true
this.mRoomToEnterLobby = typedLobby ?? ((this.insideLobby) ? this.lobby : null); use given lobby, or active lobby (if any active) or none
this.mRoomToEnterLobby = null; join random never stores the lobby. the following join will not affect the room lobby
if typedLobby is null, the server will automatically use the active lobby or default, which is what we want anyways
Operation Leave will exit any current room.
This also happens when you disconnect from the server.
Disconnect might be a step less if you don't want to create a new room on the same server.
extra logging for error debugging (helping developers with a bit of automated analysis)
use the "secret" or "token" whenever we get it. doesn't really matter if it's in AuthResponse.
this.DebugReturn(DebugLevel.ERROR, "Server returned secret. Created CustomAuthenticationValues.");
PeerState oldState = this.State;
on the NameServer, authenticate returns the MasterServer address for a region and we hop off to there
if we just "join" the game, do so. if we wanted to "create the room on demand", we have to send this to the game server as well.
on the game server, we have to apply the room properties that were chosen for creation of the room, so we use this.mRoomToGetInto
Debug.Log("GetRegions returned: " + operationResponse.ToStringFull());
PUN assumes you fetch the name-server's list of regions to ping them
is only sent by the server's response, if it has not been
sent with the client's request before!
happens only on master. on gameserver, this is a regular join (we don't need to find a random game again)
the operation OpJoinRandom either fails (with returncode 8) or returns game-to-join information
this.mListener.joinLobbyReturn();
this.LeftLobbyCleanup(); will set insideLobby = false
this.mListener.setPropertiesReturn(returnCode, debugMsg);
RemoveByteTypedPropertyKeys(actorProperties, false);
RemoveByteTypedPropertyKeys(gameProperties, false);
this.mListener.getPropertiesReturn(gameProperties, actorProperties, returnCode, debugMsg);
this usually doesn't give us a result. only if the caching is affected the server will send one.
any of the lists is null and shouldn't. print a error
this.friendListTimestamp = 1; makes sure the timestamp is not accidentally 0
Age of friend list info (in milliseconds). It's 0 until a friend list is fetched.
Request the rooms and online status for a list of friends. All client must set a unique username via PlayerName property. The result is available in this.Friends.
Used on Master Server to find the rooms played by a selected list of users.
The result will be mapped to LoadBalancingClient.Friends when available.
The list is initialized by OpFindFriends on first use (before that, it is null).
Users identify themselves by setting a PlayerName in the LoadBalancingClient instance.
This in turn will send the name in OpAuthenticate after each connect (to master and game servers).
Note: Changing a player's name doesn't make sense when using a friend list.
The list of usernames must be fetched from some other source (not provided by Photon).
Internal:
The server response includes 2 arrays of info (each index matching a friend from the request):
ParameterCode.FindFriendsResponseOnlineList = bool[] of online states
ParameterCode.FindFriendsResponseRoomIdList = string[] of room names (empty string if not in a room)
Array of friend's names (make sure they are unique).
return false; fetching friends currently, so don't do it again (avoid changing the list while fetching friends)
this.CustomAuthenticationValues.Secret = null; when connecting to NameServer, invalidate any auth values
this.IsInitialConnect = false; after handling potential initial-connect issues with special messages, we are now sure we can reach a server
this.EstablishEncryption(); always enable encryption
if we have a token we don't have to wait for encryption (it is encrypted anyways, so encryption is just optional later on)
on nameserver, the "process" is stopped here, so the developergame can either get regions or authenticate with a specific region
this client is not setup to connect to a default region. find out which regions there are!
we might need to authenticate automatically now, so the client can do anything at all
once encryption is availble, the client should send one (secure) authenticate. it includes the AppId (which identifies your app on the Photon Cloud)
this.OpAuthenticate(this.mAppId, this.mAppVersionPun, this.PlayerName, this.CustomAuthenticationValues, this.CloudRegion.ToString()); TODO: check if there are alternatives
this.CustomAuthenticationValues.Secret = null; invalidate any custom auth secrets
this.State = global::PeerState.PeerCreated; if we set another state here, we could keep clients from connecting in OnDisconnectedFromPhoton right here.
this.CustomAuthenticationValues.Secret = null; invalidate any custom auth secrets
this.CustomAuthenticationValues.Secret = null; invalidate any custom auth secrets
this.mListener.clientErrorReturn(statusCode);
this.mListener.warningReturn(statusCode);
TCP "routing" is an option of Photon that's not currently needed (or supported) by PUN
case StatusCode.TcpRouterResponseOk:
break;
case StatusCode.TcpRouterResponseEndpointUnknown:
case StatusCode.TcpRouterResponseNodeIdUnknown:
case StatusCode.TcpRouterResponseNodeNotReady:
this.DebugReturn(DebugLevel.ERROR, "Unexpected router response: " + statusCode);
break;
this.mListener.serverErrorReturn(statusCode.value());
else
{
the actor sending this event is not in actorlist. this is usually no problem
if (photonEvent.Code != (byte)LiteOpCode.Join)
{
Debug.LogWarning("Received event, but we do not have this actor: " + actorNr);
}
}
a takeover is successful automatically, if taken from current owner
not used anymore
Debug.LogInfo("Received stats!");
actorNr is fetched out of event above
this.ResetPhotonViewsOnSerialize(); This sets the correct OnSerializeState for Reliable OnSerialize
in this player's 'own' join event, we get a complete list of players in the room, so check if we know all players
joinWithCreateOnDemand can turn an OpJoin into creating the room. Then actorNumber is 1 and callback: OnCreatedRoom()
SendMonoMessage(PhotonNetworkingMessage.OnJoinedRoom); Always send OnJoinedRoom
ts: each event now contains a single RPC. execute this
Debug.Log("Ev RPC from: " + originatingPlayer);
Debug.Log(serializeData.ToStringFull());
MasterClient "requests" a disconnection from us
Debug.Log("Ev Destroy for viewId: " + instantiationId + " sent by owner: " + (instantiationId PhotonNetwork.MAX_VIEW_IDS == actorNr) + " this client is owner: " + (instantiationId PhotonNetwork.MAX_VIEW_IDS == this.mLocalActor.ID));
actorNr might be null. it is fetched out of event on top of method
Hashtable eventContent = (Hashtable) photonEvent[ParameterCode.Data];
this.mListener.customEventAction(actorNr, eventCode, eventContent);
this.OpRaiseEvent(PunEvent.VacantViewIds, true, vacantViews.ToArray());
PHOTONVIEWRPC related
Executes a received RPC event
ts: updated with "flat" event data
int netViewID = (int)rpcData[(byte)0]; LIMITS PHOTONVIEWS&PLAYERS
int otherSidePrefix = 0; by default, the prefix is 0 (and this is not being sent)
int rpcIndex = (byte)rpcData[(byte)5]; LIMITS RPC COUNT
Get method name
SetReceiving filtering
return; Ignore group
MonoBehaviour[] mbComponents = photonNetview.GetComponents
Get [RPC] methods from cache
Check cache for valid methodname+arguments
Normal, PhotonNetworkMessage left out
Check for PhotonNetworkMessage being the last
Error handling
Check if all types match with parameters. We can have more paramters then types (allow last RPC type to be different).
todo: check metro type usage
first viewID is now also the gameobject's instantiateId
int instantiateId = viewIDs[0]; LIMITS PHOTONVIEWS&PLAYERS
TODO: reduce hashtable key usage by using a parameter array for the various values
Hashtable instantiateEvent = new Hashtable(); This players info is sent via ActorID
send the list of viewIDs only if there are more than one. else the instantiateId is the viewID
instantiateEvent[(byte)4] = viewIDs; LIMITS PHOTONVIEWS&PLAYERS
instantiateEvent[(byte)8] = this.currentLevelPrefix; photonview's object's level prefix
some values always present:
SetReceiving filtering
return null; Ignore group
load prefab, if it wasn't loaded before (calling methods might do this)
now modify the loaded "blueprint" object before it becomes a part of the scene (by instantiating it)
NOTE instantiating the loaded resource will keep the viewID but would not copy instantiation data, so it's set below
so we only set the viewID and instantiationId now. the instantiationData can be fetched
load the resource and set it's values before instantiating it:
NOTE instantiating the loaded resource will keep the viewID but would not copy instantiation data, so it's set below
so we only set the viewID and instantiationId now. the instantiationData can be fetched
Send OnPhotonInstantiate callback to newly created GO.
GO will be enabled when instantiated from Prefab and it does not matter if the script is enabled or disabled.
Debug.Log("StoreInstantiationData() instantiationId: " + instantiationId + " tempInstantiationData.Count: " + tempInstantiationData.Count);
Debug.Log("FetchInstantiationData() instantiationId: " + instantiationId + " tempInstantiationData.Count: " + tempInstantiationData.Count);
Destroys all Instantiates and RPCs locally and (if not localOnly) sends EvDestroy(player) and clears related events in the server buffer.
clean server's Instantiate and RPC buffers
send Destroy(player) to anyone else
locally cleaning up that player's objects
any non-local work is already done, so with the list of that player's objects, we can clean up (locally only)
with ownership transfer, some objects might lose their owner.
in that case, the creator becomes the owner again. every client can apply this. done below.
Debug.Log("Creator is: " + view.ownerId);
This method might fail and quit early due to several tests.
GameObject to cleanup.
For localOnly, tests of control are skipped and the server is not updated.
Don't remove the GO if it doesn't have any PhotonView
int creatorId = viewZero.CreatorActorNr; creatorId of obj is needed to delete EvInstantiate (only if it's from that user)
int instantiationId = viewZero.instantiationId; actual, live InstantiationIds start with 1 and go up
Don't remove GOs that are owned by others (unless this is the master and the remote player left)
Don't remove the Instantiation from the server, if it doesn't have a proper ID
cleanup instantiation (event and local list)
this.ServerCleanInstantiateAndDestroy(instantiationId, creatorId, viewZero.isRuntimeInstantiated); server cleaning
cleanup PhotonViews and their RPCs events (if not localOnly)
we only destroyclean PhotonViews that were created by PhotonNetwork.Instantiate (and those have an instantiationId!)
This returns -1 if the GO could not be found in list of instantiatedObjects.
Removes an instantiation event from the server's cache. Needs id and actorNr of player who instantiated.
this.OpRaiseEvent(PunEvent.Instantiation, removeFilter, true, 0, new int[] { actorNr }, EventCaching.RemoveFromRoomCache);
if the view got loaded with the scene, the EvDestroy must be cached (there is no Instantiate-msg which we can remove)
reason: joining players will load the obj and have to destroy it (too)
this.OpRaiseEvent(PunEvent.DestroyPlayer, evData, true, 0, EventCaching.DoNotCache, ReceiverGroup.Others);
this.OpRaiseEvent(PunEvent.DestroyPlayer, evData, true, 0, EventCaching.DoNotCache, ReceiverGroup.Others);
removes all "Instantiation" events of player actorNr. this is not an event for anyone else
this.OpRaiseEvent(PunEvent.Instantiation, null, true, 0, new int[] { actorNr }, EventCaching.RemoveFromRoomCache);
PhotonNetwork.networkingPeer.OpRaiseEvent(PunEvent.OwnershipRequest, true, new int[] { viewID, fromOwner }, 0, EventCaching.DoNotCache, null, ReceiverGroup.All, 0);
this.OpRaiseEvent(PunEvent.OwnershipRequest, new int[] {viewID, fromOwner}, true, new RaiseEventOptions() { Receivers = ReceiverGroup.All }); All sends to all via server (including self)
PhotonNetwork.networkingPeer.OpRaiseEvent(PunEvent.OwnershipTransfer, true, new int[] {viewID, playerID}, 0, EventCaching.DoNotCache, null, ReceiverGroup.All, 0);
this.OpRaiseEvent(PunEvent.OwnershipTransfer, new int[] { viewID, playerID }, true, new RaiseEventOptions() { Receivers = ReceiverGroup.All }); All sends to all via server (including self)
don't register views with ID 0 (not initialized). they register when a ID is assigned later on
if some other view is in the list already, we got a problem. it might be undestructible. print out error
this.photonViewList.Remove(netView.viewID); TODO check if we chould Destroy the GO of this view?!
Debug.Log("adding view to known list: " + netView);
Debug.LogError("view being added. " + netView); Exit Games internal log
Will remove the view from list of views (by its ID).
public void RemovePhotonView(PhotonView netView)
{
if (!Application.isPlaying)
{
this.photonViewList = new Dictionary
return;
}
PhotonView removedView = null;
this.photonViewList.TryGetValue(netView.viewID, out removedView);
if (removedView != netView)
{
Debug.LogError("Detected two differing PhotonViews with same viewID: " + netView.viewID);
}
this.photonViewList.Remove(netView.viewID);
if (this.DebugOut >= DebugLevel.ALL)
{
this.DebugReturn(DebugLevel.ALL, "Removed PhotonView: " + netView.viewID);
}
}
Removes the RPCs of someone else (to be used as master).
This won't clean any local caches. It just tells the server to forget a player's RPCs and instantiates.
this.OpRaiseEvent(PunEvent.RPC, null, true, 0, new int[] { actorNumber }, EventCaching.RemoveFromRoomCache);
Instead removing RPCs or Instantiates, this removed everything cached by the actor.
this.OpRaiseEvent(0, null, true, 0, new int[] { actorNumber }, EventCaching.RemoveFromRoomCache);
this.OpRaiseEvent(0, null, true, 0, EventCaching.RemoveFromRoomCache, ReceiverGroup.MasterClient); TODO: check who gets this event?
This clears the cache of any playeractor who's no longer in the room (making it a simple clean-up option for a new master)
opParameters[ParameterCode.Code] = (byte)0; any event
opParameters[ParameterCode.Cache] = (byte)EventCaching.RemoveFromRoomCacheForActorsLeft; option to clear the room cache of all events of players who left
Remove RPCs of view (if they are local player's RPCs)
this.OpRaiseEvent(PunEvent.RPC, rpcFilterByViewId, true, 0, EventCaching.RemoveFromRoomCache, ReceiverGroup.Others);
TODO: should we really change the prefix for existing PVs?! better keep it!
foreach (PhotonView view in this.photonViewList.Values)
{
view.prefix = prefix;
}
return; Block sending on this group
if (view.viewID < 1) TODO: check why 0 should be illegal
ts: changed RPCs to a one-level hashtable as described in internal.txt
rpcEvent[(byte)0] = (int)view.viewID; LIMITS PHOTONVIEWS&PLAYERS
send name or shortcut (if available)
rpcEvent[(byte)5] = (byte)shortcut; LIMITS RPC COUNT
RPC Hashtable Structure
(byte)0 -> (int) ViewId (combined from actorNr and actor-unique-id)
(byte)1 -> (short) prefix (level)
(byte)2 -> (int) server timestamp
(byte)3 -> (string) methodname
(byte)4 -> (object[]) parameters
(byte)5 -> (byte) method shortcut (alternative to name)
This is sent as event (code: 200) which will contain a sender (origin of this RPC).
return; Block sending on this group
ts: changed RPCs to a one-level hashtable as described in internal.txt
rpcEvent[(byte)0] = (int)view.viewID; LIMITS NETWORKVIEWS&PLAYERS
send name or shortcut (if available)
rpcEvent[(byte)5] = (byte)shortcut; LIMITS RPC COUNT
Check scoping
Execute local
Execute local
SetReceiving
this.OpChangeGroups(disableList.Count > 0 ? disableList.ToArray() : null, enableList.Count > 0 ? enableList.ToArray() : null); Passing a 0 sized array != passing null
SetSending
this.blockSendingGroups.Add(group); can be added to HashSet no matter if already in it
Debug.Log("OnLevelWasLoaded photonViewList.Count: " + photonViewList.Count); Exit Games internal log
this is called by Update() and in Unity that means it's single threaded.
return; No need to send OnSerialize messages (these are never buffered anyway)
Fetch all sending photonViews
continue; Only on actives
continue; Only on actives
continue; Block sending on this group
Run it trough its OnSerialize
Everything has been removed by compression, nothing to send
Debug.Log(" NO OBS on " + view.name + " " + view.owner);
Send the messages: every group is send in it's own message and unreliable and reliable are split as well
calls OnPhotonSerializeView (through ExecuteOnSerialize)
the content created here is consumed by receivers in: ReadOnSerialize
each view creates a list of values that should be sent
EVDATA:
0=View ID (an int, never compressed cause it's not in the data)
1=data of observed type (different per type of observed object)
2=compressed data (in this case, key 1 is empty)
3=list of values that are actually null (if something was changed but actually IS null)
evData[(byte)1] = dataArray; this is the actual data (script or observed object)
compress content of data set (by comparing to view.lastOnSerializeDataSent)
the "original" dataArray is NOT modified by DeltaCompressionWrite
if something was compressed, the evData key 2 and 3 are used (see above)
buffer the full data set (for next compression)
Reads updates created by OnSerializeWrite
read view ID from key (byte)0: a int-array (PUN 1.17++)
SetReceiving filtering
return; Ignore group
Skip this packet as we haven't got received complete-copy of this view yet.
store last received for delta-compression usage
obviously the owner changed and we didn't yet notice.
Compares the new data with previously sent data and skips values that didn't change.
return true; all has to be sent
We can compress as we sent a full update previously (readers can re-use previous values)
no data to be sent
if new data isn't same length as before, we send the complete data-set uncompressed
compress (by using null, instead of value, which is same as before)
compressedContent[index] is already null (initialized)
value changed, we don't replace it with null
new value is null (like a compressed value): we have to mark it so it STAYS null instead of being replaced with previous value
Only send the list of compressed fields if we actually compressed 1 or more fields.
data.Remove((byte)1); remove the original data (we only send compressed data)
all values are compressed to null, we have nothing to send
data[(byte)2] = compressedContent; current, compressted data is moved to key 2 to mark it as compressed
data[(byte)3] = valuesThatAreChangedToNull.ToArray(); data that is actually null (not just cause we didn't want to send it)
return true; some data was compressed but we need to send something
reads incoming messages created by "OnSerialize"
we have a full list of data (cause key 1 is used), so return "we have uncompressed all"
Compression was applied as data[(byte)2] exists (this is the data with some fields being compressed to null)
now we also need a previous "full" list of values to restore values that are null in this msg
return false; We dont have a full match yet, we cannot work with missing values: skip this message
despite expectation, there is no compressed data in this msg. shouldn't happen. just a null check
we replace null values in this received msg unless a index is in the "changed to null" list
data[(byte)1] = compressedContents; compressedContents are now uncompressed...
Returns true if both objects are almost identical.
Used to check whether two objects are similar enough to skip an update.
if A is not B, lets check if A is almost B
one does not equal two
check if "current level" is set in props
if loaded level is not the one defined my master in props, load that level
check if "current level" is already set in props
current level is not yet in props, so this client has to set it
this.SendOutgoingCommands(); send immediately! because: in most cases the client will begin to load and not send for a while